2026-03-31 03:40:19 +00:00
"""
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
Hermes Web UI - - Shared configuration , constants , and global state .
2026-03-31 03:40:19 +00:00
Imported by all other api / * modules and by server . py .
Discovery order for all paths :
1. Explicit environment variable
2. Filesystem heuristics ( sibling checkout , parent dir , common install locations )
3. Hardened defaults relative to $ HOME
4. Fail loudly with a human - readable fix - it message if required modules are missing
"""
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
import collections
2026-04-21 00:33:03 +00:00
import copy
2026-03-31 03:40:19 +00:00
import json
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
import logging
2026-03-31 03:40:19 +00:00
import os
import sys
import threading
import time
import traceback
import uuid
from pathlib import Path
from urllib . parse import parse_qs , urlparse
# ── Basic layout ──────────────────────────────────────────────────────────────
2026-04-12 07:11:41 +00:00
HOME = Path . home ( )
2026-03-31 03:40:19 +00:00
# REPO_ROOT is the directory that contains this file's parent (api/ -> repo root)
REPO_ROOT = Path ( __file__ ) . parent . parent . resolve ( )
# ── Network config (env-overridable) ─────────────────────────────────────────
2026-04-12 07:11:41 +00:00
HOST = os . getenv ( " HERMES_WEBUI_HOST " , " 127.0.0.1 " )
PORT = int ( os . getenv ( " HERMES_WEBUI_PORT " , " 8787 " ) )
2026-03-31 03:40:19 +00:00
2026-04-10 01:08:29 +00:00
# ── TLS/HTTPS config (optional, env-overridable) ────────────────────────────
2026-04-12 07:11:41 +00:00
TLS_CERT = os . getenv ( " HERMES_WEBUI_TLS_CERT " , " " ) . strip ( ) or None
TLS_KEY = os . getenv ( " HERMES_WEBUI_TLS_KEY " , " " ) . strip ( ) or None
2026-04-10 01:08:29 +00:00
TLS_ENABLED = TLS_CERT is not None and TLS_KEY is not None
2026-03-31 03:40:19 +00:00
# ── State directory (env-overridable, never inside repo) ──────────────────────
2026-04-12 07:11:41 +00:00
STATE_DIR = (
Path ( os . getenv ( " HERMES_WEBUI_STATE_DIR " , str ( HOME / " .hermes " / " webui " ) ) )
. expanduser ( )
. resolve ( )
)
SESSION_DIR = STATE_DIR / " sessions "
WORKSPACES_FILE = STATE_DIR / " workspaces.json "
SESSION_INDEX_FILE = SESSION_DIR / " _index.json "
SETTINGS_FILE = STATE_DIR / " settings.json "
LAST_WORKSPACE_FILE = STATE_DIR / " last_workspace.txt "
PROJECTS_FILE = STATE_DIR / " projects.json "
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger = logging . getLogger ( __name__ )
2026-03-31 03:40:19 +00:00
# ── Hermes agent directory discovery ─────────────────────────────────────────
def _discover_agent_dir ( ) - > Path :
"""
Locate the hermes - agent checkout using a multi - strategy search .
Priority :
1. HERMES_WEBUI_AGENT_DIR env var - - explicit override always wins
2. HERMES_HOME / hermes - agent - - e . g . ~ / . hermes / hermes - agent
3. Sibling of this repo - - . . / hermes - agent
4. Parent of this repo - - . . / . . / hermes - agent ( nested layout )
5. Common install paths - - ~ / . hermes / hermes - agent ( again as fallback )
6. HOME / hermes - agent - - ~ / hermes - agent ( simple flat layout )
"""
candidates = [ ]
# 1. Explicit env var
2026-04-12 07:11:41 +00:00
if os . getenv ( " HERMES_WEBUI_AGENT_DIR " ) :
candidates . append (
Path ( os . getenv ( " HERMES_WEBUI_AGENT_DIR " ) ) . expanduser ( ) . resolve ( )
)
2026-03-31 03:40:19 +00:00
# 2. HERMES_HOME / hermes-agent
2026-04-12 07:11:41 +00:00
hermes_home = os . getenv ( " HERMES_HOME " , str ( HOME / " .hermes " ) )
candidates . append ( Path ( hermes_home ) . expanduser ( ) / " hermes-agent " )
2026-03-31 03:40:19 +00:00
# 3. Sibling: <repo-root>/../hermes-agent
2026-04-12 07:11:41 +00:00
candidates . append ( REPO_ROOT . parent / " hermes-agent " )
2026-03-31 03:40:19 +00:00
# 4. Parent is the agent repo itself (repo cloned inside hermes-agent/)
2026-04-12 07:11:41 +00:00
if ( REPO_ROOT . parent / " run_agent.py " ) . exists ( ) :
2026-03-31 03:40:19 +00:00
candidates . append ( REPO_ROOT . parent )
# 5. ~/.hermes/hermes-agent (explicit common path)
2026-04-12 07:11:41 +00:00
candidates . append ( HOME / " .hermes " / " hermes-agent " )
2026-03-31 03:40:19 +00:00
# 6. ~/hermes-agent
2026-04-12 07:11:41 +00:00
candidates . append ( HOME / " hermes-agent " )
2026-03-31 03:40:19 +00:00
for path in candidates :
2026-04-12 07:11:41 +00:00
if path . exists ( ) and ( path / " run_agent.py " ) . exists ( ) :
2026-03-31 03:40:19 +00:00
return path . resolve ( )
return None
def _discover_python ( agent_dir : Path ) - > str :
"""
Locate a Python executable that has the Hermes agent dependencies installed .
Priority :
1. HERMES_WEBUI_PYTHON env var
2. Agent venv at < agent_dir > / venv / bin / python
3. Local . venv inside this repo
4. System python3
"""
2026-04-12 07:11:41 +00:00
if os . getenv ( " HERMES_WEBUI_PYTHON " ) :
return os . getenv ( " HERMES_WEBUI_PYTHON " )
2026-03-31 03:40:19 +00:00
if agent_dir :
2026-04-12 07:11:41 +00:00
venv_py = agent_dir / " venv " / " bin " / " python "
2026-03-31 03:40:19 +00:00
if venv_py . exists ( ) :
return str ( venv_py )
2026-04-24 17:45:23 +00:00
venv_py = agent_dir / " .venv " / " bin " / " python "
if venv_py . exists ( ) :
return str ( venv_py )
2026-03-31 03:40:19 +00:00
# Windows layout
2026-04-12 07:11:41 +00:00
venv_py_win = agent_dir / " venv " / " Scripts " / " python.exe "
2026-03-31 03:40:19 +00:00
if venv_py_win . exists ( ) :
return str ( venv_py_win )
2026-04-24 17:45:23 +00:00
venv_py_win = agent_dir / " .venv " / " Scripts " / " python.exe "
if venv_py_win . exists ( ) :
return str ( venv_py_win )
2026-03-31 03:40:19 +00:00
# Local .venv inside this repo
2026-04-12 07:11:41 +00:00
local_venv = REPO_ROOT / " .venv " / " bin " / " python "
2026-03-31 03:40:19 +00:00
if local_venv . exists ( ) :
return str ( local_venv )
# Fall back to system python3
import shutil
2026-04-12 07:11:41 +00:00
for name in ( " python3 " , " python " ) :
2026-03-31 03:40:19 +00:00
found = shutil . which ( name )
if found :
return found
2026-04-12 07:11:41 +00:00
return " python3 "
2026-03-31 03:40:19 +00:00
# Run discovery
_AGENT_DIR = _discover_agent_dir ( )
PYTHON_EXE = _discover_python ( _AGENT_DIR )
2026-04-04 17:59:33 +00:00
# ── Inject agent dir into sys.path so Hermes modules are importable ──────────
# When users (or CI builds) run `pip install --target .` or
# `pip install -t .` inside the hermes-agent checkout, third-party
# package directories (openai/, pydantic/, requests/, etc.) end up
# alongside real Hermes source files. Putting _AGENT_DIR at the
# FRONT of sys.path means Python resolves `import pydantic` from that
# local directory — which breaks whenever the host platform differs
# from the container (e.g. macOS .so files inside a Linux image).
#
# Fix: insert _AGENT_DIR at the END of sys.path. Python searches
# entries in order, so site-packages resolves pip packages correctly,
# and Hermes-specific modules (run_agent, hermes/, etc.) still
# resolve because they do not exist in site-packages.
2026-03-31 03:40:19 +00:00
if _AGENT_DIR is not None :
if str ( _AGENT_DIR ) not in sys . path :
2026-04-04 17:59:33 +00:00
sys . path . append ( str ( _AGENT_DIR ) )
2026-03-31 03:40:19 +00:00
_HERMES_FOUND = True
else :
_HERMES_FOUND = False
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
# ── Config file (reloadable -- supports profile switching) ──────────────────
_cfg_cache = { }
_cfg_lock = threading . Lock ( )
2026-04-16 18:55:18 +00:00
_cfg_mtime : float = 0.0 # last known mtime of config.yaml; 0 = never loaded
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
2026-04-12 07:11:41 +00:00
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
def _get_config_path ( ) - > Path :
""" Return config.yaml path for the active profile. """
2026-04-12 07:11:41 +00:00
env_override = os . getenv ( " HERMES_CONFIG_PATH " )
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
if env_override :
return Path ( env_override ) . expanduser ( )
try :
from api . profiles import get_active_hermes_home
2026-04-12 07:11:41 +00:00
return get_active_hermes_home ( ) / " config.yaml "
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
except ImportError :
2026-04-12 07:11:41 +00:00
return HOME / " .hermes " / " config.yaml "
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
def get_config ( ) - > dict :
""" Return the cached config dict, loading from disk if needed. """
if not _cfg_cache :
reload_config ( )
return _cfg_cache
2026-04-12 07:11:41 +00:00
2026-04-05 06:30:20 +00:00
def reload_config ( ) - > None :
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
""" Reload config.yaml from the active profile ' s directory. """
2026-04-16 18:55:18 +00:00
global _cfg_mtime
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
with _cfg_lock :
_cfg_cache . clear ( )
config_path = _get_config_path ( )
try :
import yaml as _yaml
2026-04-12 07:11:41 +00:00
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
if config_path . exists ( ) :
2026-04-19 04:22:28 +00:00
loaded = _yaml . safe_load ( config_path . read_text ( encoding = " utf-8 " ) )
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
if isinstance ( loaded , dict ) :
_cfg_cache . update ( loaded )
2026-04-16 18:55:18 +00:00
try :
_cfg_mtime = Path ( config_path ) . stat ( ) . st_mtime
except OSError :
_cfg_mtime = 0.0
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Failed to load yaml config from %s " , config_path )
2026-03-31 03:40:19 +00:00
2026-04-12 07:11:41 +00:00
2026-04-20 22:12:01 +00:00
def _load_yaml_config_file ( config_path : Path ) - > dict :
try :
import yaml as _yaml
except ImportError :
return { }
if not config_path . exists ( ) :
return { }
try :
loaded = _yaml . safe_load ( config_path . read_text ( encoding = " utf-8 " ) )
return loaded if isinstance ( loaded , dict ) else { }
except Exception :
logger . debug ( " Failed to parse yaml config from %s " , config_path )
return { }
def _save_yaml_config_file ( config_path : Path , config_data : dict ) - > None :
try :
import yaml as _yaml
except ImportError as exc :
raise RuntimeError ( " PyYAML is required to write Hermes config.yaml " ) from exc
config_path . parent . mkdir ( parents = True , exist_ok = True )
config_path . write_text (
_yaml . safe_dump ( config_data , sort_keys = False , allow_unicode = True ) ,
encoding = " utf-8 " ,
)
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
# Initial load
reload_config ( )
cfg = _cfg_cache # alias for backward compat with existing references
2026-03-31 03:40:19 +00:00
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
# ── Default workspace discovery ───────────────────────────────────────────────
2026-04-13 21:28:24 +00:00
def _workspace_candidates ( raw : str | Path | None = None ) - > list [ Path ] :
""" Return ordered candidate workspace paths, de-duplicated. """
candidates : list [ Path ] = [ ]
def add ( candidate : str | Path | None ) - > None :
if candidate in ( None , " " ) :
return
try :
path = Path ( candidate ) . expanduser ( ) . resolve ( )
except Exception :
return
if path not in candidates :
candidates . append ( path )
add ( raw )
if os . getenv ( " HERMES_WEBUI_DEFAULT_WORKSPACE " ) :
add ( os . getenv ( " HERMES_WEBUI_DEFAULT_WORKSPACE " ) )
home_workspace = HOME / " workspace "
home_work = HOME / " work "
if home_workspace . exists ( ) :
add ( home_workspace )
if home_work . exists ( ) :
add ( home_work )
add ( home_workspace )
add ( STATE_DIR / " workspace " )
return candidates
def _ensure_workspace_dir ( path : Path ) - > bool :
""" Best-effort check that a workspace directory exists and is writable. """
try :
path = path . expanduser ( ) . resolve ( )
path . mkdir ( parents = True , exist_ok = True )
return path . is_dir ( ) and os . access ( path , os . R_OK | os . W_OK | os . X_OK )
except Exception :
return False
def resolve_default_workspace ( raw : str | Path | None = None ) - > Path :
""" Return the first usable workspace path, creating it when possible. """
for candidate in _workspace_candidates ( raw ) :
if _ensure_workspace_dir ( candidate ) :
return candidate
raise RuntimeError (
" Could not create or access any usable workspace directory. "
" Set HERMES_WEBUI_DEFAULT_WORKSPACE to a writable path. "
)
2026-03-31 03:40:19 +00:00
def _discover_default_workspace ( ) - > Path :
"""
Resolve the default workspace in order :
1. HERMES_WEBUI_DEFAULT_WORKSPACE env var
2026-04-13 21:28:24 +00:00
2. ~ / workspace if it already exists
3. ~ / work if it already exists
4. ~ / workspace ( create if needed )
5. STATE_DIR / workspace
2026-03-31 03:40:19 +00:00
"""
2026-04-13 21:28:24 +00:00
return resolve_default_workspace ( )
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
DEFAULT_WORKSPACE = _discover_default_workspace ( )
2026-04-18 06:46:43 +00:00
DEFAULT_MODEL = os . getenv ( " HERMES_WEBUI_DEFAULT_MODEL " , " " ) # Empty = use provider default; avoids showing unavailable OpenAI model to non-OpenAI users (#646)
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
# ── Startup diagnostics ───────────────────────────────────────────────────────
2026-04-05 06:30:20 +00:00
def print_startup_config ( ) - > None :
2026-03-31 03:40:19 +00:00
""" Print detected configuration at startup so the user can verify what was found. """
2026-04-12 07:11:41 +00:00
ok = " \033 [32m[ok] \033 [0m "
warn = " \033 [33m[!!] \033 [0m "
err = " \033 [31m[XX] \033 [0m "
2026-03-31 03:40:19 +00:00
lines = [
2026-04-12 07:11:41 +00:00
" " ,
" Hermes Web UI -- startup config " ,
" -------------------------------- " ,
f " repo root : { REPO_ROOT } " ,
f " agent dir : { _AGENT_DIR if _AGENT_DIR else ' NOT FOUND ' } { ok if _AGENT_DIR else err } " ,
f " python : { PYTHON_EXE } " ,
f " state dir : { STATE_DIR } " ,
f " workspace : { DEFAULT_WORKSPACE } " ,
f " host:port : { HOST } : { PORT } " ,
f " config file : { _get_config_path ( ) } { ' (found) ' if _get_config_path ( ) . exists ( ) else ' (not found, using defaults) ' } " ,
" " ,
2026-03-31 03:40:19 +00:00
]
2026-04-12 07:11:41 +00:00
print ( " \n " . join ( lines ) , flush = True )
2026-03-31 03:40:19 +00:00
if not _HERMES_FOUND :
print (
2026-04-12 07:11:41 +00:00
f " { err } Could not find the Hermes agent directory. \n "
" The server will start but agent features will not work. \n "
" \n "
" To fix, set one of: \n "
" export HERMES_WEBUI_AGENT_DIR=/path/to/hermes-agent \n "
" export HERMES_HOME=/path/to/.hermes \n "
" \n "
" Or clone hermes-agent as a sibling of this repo: \n "
" git clone <hermes-agent-repo> ../hermes-agent \n " ,
flush = True ,
2026-03-31 03:40:19 +00:00
)
2026-04-12 07:11:41 +00:00
2026-04-05 06:30:20 +00:00
def verify_hermes_imports ( ) - > tuple :
2026-03-31 03:40:19 +00:00
"""
Attempt to import the key Hermes modules .
2026-04-04 17:59:33 +00:00
Returns ( ok : bool , missing : list [ str ] , errors : dict [ str , str ] ) .
2026-03-31 03:40:19 +00:00
"""
2026-04-12 07:11:41 +00:00
required = [ " run_agent " ]
missing = [ ]
errors = { }
2026-03-31 03:40:19 +00:00
for mod in required :
try :
__import__ ( mod )
2026-04-04 17:59:33 +00:00
except Exception as e :
2026-03-31 03:40:19 +00:00
missing . append ( mod )
2026-04-04 17:59:33 +00:00
# Capture the full error message so startup logs show WHY
# (e.g. pydantic_core .so mismatch) instead of just the name.
errors [ mod ] = f " { type ( e ) . __name__ } : { e } "
return ( len ( missing ) == 0 ) , missing , errors
2026-03-31 03:40:19 +00:00
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
# ── Limits ───────────────────────────────────────────────────────────────────
2026-04-12 07:11:41 +00:00
MAX_FILE_BYTES = 200_000
2026-03-31 03:40:19 +00:00
MAX_UPLOAD_BYTES = 20 * 1024 * 1024
# ── File type maps ───────────────────────────────────────────────────────────
2026-04-12 07:11:41 +00:00
IMAGE_EXTS = { " .png " , " .jpg " , " .jpeg " , " .gif " , " .svg " , " .webp " , " .ico " , " .bmp " }
MD_EXTS = { " .md " , " .markdown " , " .mdown " }
CODE_EXTS = {
" .py " ,
" .js " ,
" .ts " ,
" .jsx " ,
" .tsx " ,
" .css " ,
" .html " ,
" .json " ,
" .yaml " ,
" .yml " ,
" .toml " ,
" .sh " ,
" .bash " ,
" .txt " ,
" .log " ,
" .env " ,
" .csv " ,
" .xml " ,
" .sql " ,
" .rs " ,
" .go " ,
" .java " ,
" .c " ,
" .cpp " ,
" .h " ,
}
2026-03-31 03:40:19 +00:00
MIME_MAP = {
2026-04-12 07:11:41 +00:00
" .png " : " image/png " ,
" .jpg " : " image/jpeg " ,
" .jpeg " : " image/jpeg " ,
" .gif " : " image/gif " ,
" .svg " : " image/svg+xml " ,
" .webp " : " image/webp " ,
" .ico " : " image/x-icon " ,
" .bmp " : " image/bmp " ,
" .pdf " : " application/pdf " ,
" .json " : " application/json " ,
2026-04-16 05:43:31 +00:00
" .xls " : " application/vnd.ms-excel " ,
" .xlsx " : " application/vnd.openxmlformats-officedocument.spreadsheetml.sheet " ,
" .doc " : " application/msword " ,
" .docx " : " application/vnd.openxmlformats-officedocument.wordprocessingml.document " ,
2026-03-31 03:40:19 +00:00
}
# ── Toolsets (from config.yaml or hardcoded default) ─────────────────────────
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
_DEFAULT_TOOLSETS = [
2026-04-12 07:11:41 +00:00
" browser " ,
" clarify " ,
" code_execution " ,
" cronjob " ,
" delegation " ,
" file " ,
" image_gen " ,
" memory " ,
" session_search " ,
" skills " ,
" terminal " ,
" todo " ,
" web " ,
" webhook " ,
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
]
2026-04-16 06:39:07 +00:00
def _resolve_cli_toolsets ( cfg = None ) :
""" Resolve CLI toolsets using the agent ' s _get_platform_tools() so that
MCP server toolsets are automatically included , matching CLI behaviour . """
if cfg is None :
cfg = get_config ( )
try :
from hermes_cli . tools_config import _get_platform_tools
return list ( _get_platform_tools ( cfg , " cli " ) )
except Exception :
# Fallback: read raw list from config (MCP toolsets will be missing)
return cfg . get ( " platform_toolsets " , { } ) . get ( " cli " , _DEFAULT_TOOLSETS )
CLI_TOOLSETS = _resolve_cli_toolsets ( )
2026-03-31 03:40:19 +00:00
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# ── Model / provider discovery ───────────────────────────────────────────────
# Hardcoded fallback models (used when no config.yaml or agent is available)
2026-04-15 22:20:25 +00:00
# Also used as the OpenRouter model list — keep this curated to current, widely-used models.
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
_FALLBACK_MODELS = [
2026-04-15 22:27:55 +00:00
# OpenAI
{ " provider " : " OpenAI " , " id " : " openai/gpt-5.4-mini " , " label " : " GPT-5.4 Mini " } ,
{ " provider " : " OpenAI " , " id " : " openai/gpt-5.4 " , " label " : " GPT-5.4 " } ,
# Anthropic — 4.6 flagship + 4.5 generation
{ " provider " : " Anthropic " , " id " : " anthropic/claude-opus-4.6 " , " label " : " Claude Opus 4.6 " } ,
{ " provider " : " Anthropic " , " id " : " anthropic/claude-sonnet-4.6 " , " label " : " Claude Sonnet 4.6 " } ,
{ " provider " : " Anthropic " , " id " : " anthropic/claude-sonnet-4-5 " , " label " : " Claude Sonnet 4.5 " } ,
{ " provider " : " Anthropic " , " id " : " anthropic/claude-haiku-4-5 " , " label " : " Claude Haiku 4.5 " } ,
2026-04-18 17:09:59 +00:00
# Google — 3.x (latest preview) + 2.5 (stable GA)
{ " provider " : " Google " , " id " : " google/gemini-3.1-pro-preview " , " label " : " Gemini 3.1 Pro Preview " } ,
{ " provider " : " Google " , " id " : " google/gemini-3-flash-preview " , " label " : " Gemini 3 Flash Preview " } ,
{ " provider " : " Google " , " id " : " google/gemini-3.1-flash-lite-preview " , " label " : " Gemini 3.1 Flash Lite Preview " } ,
{ " provider " : " Google " , " id " : " google/gemini-2.5-pro " , " label " : " Gemini 2.5 Pro " } ,
{ " provider " : " Google " , " id " : " google/gemini-2.5-flash " , " label " : " Gemini 2.5 Flash " } ,
2026-04-15 22:27:55 +00:00
# DeepSeek
{ " provider " : " DeepSeek " , " id " : " deepseek/deepseek-chat-v3-0324 " , " label " : " DeepSeek V3 " } ,
{ " provider " : " DeepSeek " , " id " : " deepseek/deepseek-r1 " , " label " : " DeepSeek R1 " } ,
2026-04-15 22:54:18 +00:00
# Qwen (Alibaba) — strong coding and general models
{ " provider " : " Qwen " , " id " : " qwen/qwen3-coder " , " label " : " Qwen3 Coder " } ,
{ " provider " : " Qwen " , " id " : " qwen/qwen3.6-plus " , " label " : " Qwen3.6 Plus " } ,
# xAI
2026-04-15 23:00:29 +00:00
{ " provider " : " xAI " , " id " : " x-ai/grok-4.20 " , " label " : " Grok 4.20 " } ,
2026-04-15 22:27:55 +00:00
# Mistral
{ " provider " : " Mistral " , " id " : " mistralai/mistral-large-latest " , " label " : " Mistral Large " } ,
2026-04-18 07:18:20 +00:00
# MiniMax
{ " provider " : " MiniMax " , " id " : " minimax/MiniMax-M2.7 " , " label " : " MiniMax M2.7 " } ,
{ " provider " : " MiniMax " , " id " : " minimax/MiniMax-M2.7-highspeed " , " label " : " MiniMax M2.7 Highspeed " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
]
# Provider display names for known Hermes provider IDs
_PROVIDER_DISPLAY = {
2026-04-12 07:11:41 +00:00
" nous " : " Nous Portal " ,
" openrouter " : " OpenRouter " ,
" anthropic " : " Anthropic " ,
" openai " : " OpenAI " ,
" openai-codex " : " OpenAI Codex " ,
" copilot " : " GitHub Copilot " ,
" zai " : " Z.AI / GLM " ,
" kimi-coding " : " Kimi / Moonshot " ,
" deepseek " : " DeepSeek " ,
" minimax " : " MiniMax " ,
" google " : " Google " ,
" meta-llama " : " Meta Llama " ,
" huggingface " : " HuggingFace " ,
" alibaba " : " Alibaba " ,
" ollama " : " Ollama " ,
2026-04-22 20:18:02 +00:00
" ollama-cloud " : " Ollama Cloud " ,
2026-04-14 01:46:11 +00:00
" opencode-zen " : " OpenCode Zen " ,
" opencode-go " : " OpenCode Go " ,
2026-04-12 07:11:41 +00:00
" lmstudio " : " LM Studio " ,
2026-04-15 22:27:55 +00:00
" mistralai " : " Mistral " ,
2026-04-15 22:54:18 +00:00
" qwen " : " Qwen " ,
" x-ai " : " xAI " ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
}
2026-04-22 00:24:54 +00:00
# Provider alias → canonical slug. Users configure providers using the
# dotted/hyphenated form they see on the provider website (``z.ai``,
# ``x.ai``, ``google``) but the internal catalog (``_PROVIDER_MODELS``)
# uses slugs without punctuation (``zai``, ``xai``, ``gemini``). Without
# normalisation the provider lands in the ``else`` branch of the group
# builder and no models are returned — the bug behind #815.
#
# This table is authoritative for the WebUI. When ``hermes_cli.models``
# is importable we also merge its ``_PROVIDER_ALIASES`` on top so any
# new aliases added to the agent automatically apply. Keeping the local
# copy means the fix works even in environments where the agent tree is
# not on ``sys.path`` (CI, installs without hermes-agent cloned
# alongside the WebUI).
_PROVIDER_ALIASES = {
" glm " : " zai " ,
" z-ai " : " zai " ,
" z.ai " : " zai " ,
" zhipu " : " zai " ,
" github " : " copilot " ,
" github-copilot " : " copilot " ,
" github-models " : " copilot " ,
" github-model " : " copilot " ,
" google " : " gemini " ,
" google-gemini " : " gemini " ,
" google-ai-studio " : " gemini " ,
" kimi " : " kimi-coding " ,
" moonshot " : " kimi-coding " ,
" claude " : " anthropic " ,
" claude-code " : " anthropic " ,
" deep-seek " : " deepseek " ,
" opencode " : " opencode-zen " ,
" grok " : " xai " ,
" x-ai " : " xai " ,
" x.ai " : " xai " ,
" aws " : " bedrock " ,
" aws-bedrock " : " bedrock " ,
" amazon " : " bedrock " ,
" amazon-bedrock " : " bedrock " ,
" qwen " : " alibaba " ,
" aliyun " : " alibaba " ,
" dashscope " : " alibaba " ,
" alibaba-cloud " : " alibaba " ,
}
def _resolve_provider_alias ( name : str ) - > str :
""" Return the canonical provider slug for *name*.
Applies the WebUI ' s local alias table first, then merges any
additional aliases the agent provides ( when hermes_cli is on
sys . path ) . Lookup is case - insensitive and whitespace - trimmed .
Unknown names pass through unchanged .
"""
if not name :
return name
raw = str ( name ) . strip ( ) . lower ( )
# Prefer the agent's table when available so new aliases added there
# work automatically; otherwise fall through to our local copy.
try :
from hermes_cli . models import _PROVIDER_ALIASES as _agent_aliases
if raw in _agent_aliases :
return _agent_aliases [ raw ]
except Exception :
pass
return _PROVIDER_ALIASES . get ( raw , name )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# Well-known models per provider (used to populate dropdown for direct API providers)
_PROVIDER_MODELS = {
2026-04-12 07:11:41 +00:00
" anthropic " : [
{ " id " : " claude-opus-4.6 " , " label " : " Claude Opus 4.6 " } ,
{ " id " : " claude-sonnet-4.6 " , " label " : " Claude Sonnet 4.6 " } ,
{ " id " : " claude-sonnet-4-5 " , " label " : " Claude Sonnet 4.5 " } ,
{ " id " : " claude-haiku-4-5 " , " label " : " Claude Haiku 4.5 " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" openai " : [
{ " id " : " gpt-5.4-mini " , " label " : " GPT-5.4 Mini " } ,
2026-04-15 22:20:25 +00:00
{ " id " : " gpt-5.4 " , " label " : " GPT-5.4 " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" openai-codex " : [
2026-04-14 05:35:27 +00:00
{ " id " : " gpt-5.4 " , " label " : " GPT-5.4 " } ,
{ " id " : " gpt-5.4-mini " , " label " : " GPT-5.4 Mini " } ,
{ " id " : " gpt-5.3-codex " , " label " : " GPT-5.3 Codex " } ,
{ " id " : " gpt-5.2-codex " , " label " : " GPT-5.2 Codex " } ,
{ " id " : " gpt-5.1-codex-max " , " label " : " GPT-5.1 Codex Max " } ,
{ " id " : " gpt-5.1-codex-mini " , " label " : " GPT-5.1 Codex Mini " } ,
{ " id " : " codex-mini-latest " , " label " : " Codex Mini (latest) " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" google " : [
2026-04-18 17:09:59 +00:00
{ " id " : " gemini-3.1-pro-preview " , " label " : " Gemini 3.1 Pro Preview " } ,
{ " id " : " gemini-3-flash-preview " , " label " : " Gemini 3 Flash Preview " } ,
{ " id " : " gemini-3.1-flash-lite-preview " , " label " : " Gemini 3.1 Flash Lite Preview " } ,
{ " id " : " gemini-2.5-pro " , " label " : " Gemini 2.5 Pro " } ,
{ " id " : " gemini-2.5-flash " , " label " : " Gemini 2.5 Flash " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" deepseek " : [
{ " id " : " deepseek-chat-v3-0324 " , " label " : " DeepSeek V3 " } ,
{ " id " : " deepseek-reasoner " , " label " : " DeepSeek Reasoner " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" nous " : [
2026-04-23 05:56:21 +00:00
{ " id " : " @nous:anthropic/claude-opus-4.6 " , " label " : " Claude Opus 4.6 (via Nous) " } ,
{ " id " : " @nous:anthropic/claude-sonnet-4.6 " , " label " : " Claude Sonnet 4.6 (via Nous) " } ,
{ " id " : " @nous:openai/gpt-5.4-mini " , " label " : " GPT-5.4 Mini (via Nous) " } ,
{ " id " : " @nous:google/gemini-3.1-pro-preview " , " label " : " Gemini 3.1 Pro Preview (via Nous) " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" zai " : [
{ " id " : " glm-5.1 " , " label " : " GLM-5.1 " } ,
{ " id " : " glm-5 " , " label " : " GLM-5 " } ,
{ " id " : " glm-5-turbo " , " label " : " GLM-5 Turbo " } ,
{ " id " : " glm-4.7 " , " label " : " GLM-4.7 " } ,
{ " id " : " glm-4.5 " , " label " : " GLM-4.5 " } ,
{ " id " : " glm-4.5-flash " , " label " : " GLM-4.5 Flash " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" kimi-coding " : [
{ " id " : " moonshot-v1-8k " , " label " : " Moonshot v1 8k " } ,
{ " id " : " moonshot-v1-32k " , " label " : " Moonshot v1 32k " } ,
{ " id " : " moonshot-v1-128k " , " label " : " Moonshot v1 128k " } ,
{ " id " : " kimi-latest " , " label " : " Kimi Latest " } ,
release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755)
## Batch release: v0.50.102 – v0.50.108
Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch.
### Included fixes
| Version | PR | What it fixes |
|---|---|---|
| v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) |
| v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) |
| v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) |
| v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) |
| v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests |
| v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) |
| v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) |
### Testing
- **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded)
- **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`)
- **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified
All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
2026-04-20 07:26:55 +00:00
{ " id " : " kimi-k2.5 " , " label " : " Kimi K2.5 " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-12 07:11:41 +00:00
" minimax " : [
{ " id " : " MiniMax-M2.7 " , " label " : " MiniMax M2.7 " } ,
{ " id " : " MiniMax-M2.7-highspeed " , " label " : " MiniMax M2.7 Highspeed " } ,
{ " id " : " MiniMax-M2.5 " , " label " : " MiniMax M2.5 " } ,
{ " id " : " MiniMax-M2.5-highspeed " , " label " : " MiniMax M2.5 Highspeed " } ,
{ " id " : " MiniMax-M2.1 " , " label " : " MiniMax M2.1 " } ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
] ,
2026-04-06 21:29:06 +00:00
# GitHub Copilot — model IDs served via the Copilot API
2026-04-12 07:11:41 +00:00
" copilot " : [
{ " id " : " gpt-5.4 " , " label " : " GPT-5.4 " } ,
{ " id " : " gpt-5.4-mini " , " label " : " GPT-5.4 Mini " } ,
{ " id " : " gpt-4o " , " label " : " GPT-4o " } ,
{ " id " : " claude-opus-4.6 " , " label " : " Claude Opus 4.6 " } ,
{ " id " : " claude-sonnet-4.6 " , " label " : " Claude Sonnet 4.6 " } ,
2026-04-18 17:09:59 +00:00
{ " id " : " gemini-3-flash-preview " , " label " : " Gemini 3 Flash Preview " } ,
2026-04-06 21:29:06 +00:00
] ,
2026-04-14 01:46:11 +00:00
# OpenCode Zen — curated models via opencode.ai/zen (pay-as-you-go credits)
" opencode-zen " : [
{ " id " : " gpt-5.4-pro " , " label " : " GPT-5.4 Pro " } ,
{ " id " : " gpt-5.4 " , " label " : " GPT-5.4 " } ,
{ " id " : " gpt-5.4-mini " , " label " : " GPT-5.4 Mini " } ,
{ " id " : " gpt-5.4-nano " , " label " : " GPT-5.4 Nano " } ,
{ " id " : " gpt-5.3-codex " , " label " : " GPT-5.3 Codex " } ,
{ " id " : " gpt-5.3-codex-spark " , " label " : " GPT-5.3 Codex Spark " } ,
{ " id " : " gpt-5.2 " , " label " : " GPT-5.2 " } ,
{ " id " : " gpt-5.2-codex " , " label " : " GPT-5.2 Codex " } ,
{ " id " : " gpt-5.1 " , " label " : " GPT-5.1 " } ,
{ " id " : " gpt-5.1-codex " , " label " : " GPT-5.1 Codex " } ,
{ " id " : " gpt-5.1-codex-max " , " label " : " GPT-5.1 Codex Max " } ,
{ " id " : " gpt-5.1-codex-mini " , " label " : " GPT-5.1 Codex Mini " } ,
{ " id " : " gpt-5 " , " label " : " GPT-5 " } ,
{ " id " : " gpt-5-codex " , " label " : " GPT-5 Codex " } ,
{ " id " : " gpt-5-nano " , " label " : " GPT-5 Nano " } ,
{ " id " : " claude-opus-4-6 " , " label " : " Claude Opus 4.6 " } ,
{ " id " : " claude-opus-4-5 " , " label " : " Claude Opus 4.5 " } ,
{ " id " : " claude-opus-4-1 " , " label " : " Claude Opus 4.1 " } ,
{ " id " : " claude-sonnet-4-6 " , " label " : " Claude Sonnet 4.6 " } ,
{ " id " : " claude-sonnet-4-5 " , " label " : " Claude Sonnet 4.5 " } ,
{ " id " : " claude-sonnet-4 " , " label " : " Claude Sonnet 4 " } ,
{ " id " : " claude-haiku-4-5 " , " label " : " Claude Haiku 4.5 " } ,
{ " id " : " claude-3-5-haiku " , " label " : " Claude 3.5 Haiku " } ,
2026-04-15 23:00:29 +00:00
{ " id " : " gemini-3.1-pro-preview " , " label " : " Gemini 3.1 Pro Preview " } ,
{ " id " : " gemini-3-flash-preview " , " label " : " Gemini 3 Flash Preview " } ,
2026-04-18 17:09:59 +00:00
{ " id " : " gemini-3.1-flash-lite-preview " , " label " : " Gemini 3.1 Flash Lite Preview " } ,
{ " id " : " gemini-2.5-pro " , " label " : " Gemini 2.5 Pro " } ,
{ " id " : " gemini-2.5-flash " , " label " : " Gemini 2.5 Flash " } ,
2026-04-14 01:46:11 +00:00
{ " id " : " glm-5.1 " , " label " : " GLM-5.1 " } ,
{ " id " : " glm-5 " , " label " : " GLM-5 " } ,
{ " id " : " kimi-k2.5 " , " label " : " Kimi K2.5 " } ,
{ " id " : " minimax-m2.5 " , " label " : " MiniMax M2.5 " } ,
{ " id " : " minimax-m2.5-free " , " label " : " MiniMax M2.5 Free " } ,
{ " id " : " nemotron-3-super-free " , " label " : " Nemotron 3 Super Free " } ,
{ " id " : " big-pickle " , " label " : " Big Pickle " } ,
] ,
# OpenCode Go — flat-rate models via opencode.ai/go ($10/month)
" opencode-go " : [
{ " id " : " glm-5.1 " , " label " : " GLM-5.1 " } ,
{ " id " : " glm-5 " , " label " : " GLM-5 " } ,
{ " id " : " kimi-k2.5 " , " label " : " Kimi K2.5 " } ,
{ " id " : " mimo-v2-pro " , " label " : " MiMo V2 Pro " } ,
{ " id " : " mimo-v2-omni " , " label " : " MiMo V2 Omni " } ,
{ " id " : " minimax-m2.7 " , " label " : " MiniMax M2.7 " } ,
{ " id " : " minimax-m2.5 " , " label " : " MiniMax M2.5 " } ,
] ,
2026-04-06 21:29:06 +00:00
# 'gemini' is the hermes_cli provider ID for Google AI Studio
2026-04-18 17:09:59 +00:00
# Model IDs are bare — sent directly to:
# https://generativelanguage.googleapis.com/v1beta/openai/chat/completions
2026-04-12 07:11:41 +00:00
" gemini " : [
2026-04-18 17:09:59 +00:00
{ " id " : " gemini-3.1-pro-preview " , " label " : " Gemini 3.1 Pro Preview " } ,
{ " id " : " gemini-3-flash-preview " , " label " : " Gemini 3 Flash Preview " } ,
{ " id " : " gemini-3.1-flash-lite-preview " , " label " : " Gemini 3.1 Flash Lite Preview " } ,
{ " id " : " gemini-2.5-pro " , " label " : " Gemini 2.5 Pro " } ,
{ " id " : " gemini-2.5-flash " , " label " : " Gemini 2.5 Flash " } ,
2026-04-06 21:29:06 +00:00
] ,
2026-04-15 22:27:55 +00:00
# Mistral — prefix used in OpenRouter model IDs (mistralai/mistral-large-latest)
" mistralai " : [
{ " id " : " mistral-large-latest " , " label " : " Mistral Large " } ,
{ " id " : " mistral-small-latest " , " label " : " Mistral Small " } ,
] ,
2026-04-15 22:54:18 +00:00
# Qwen (Alibaba) — prefix used in OpenRouter model IDs (qwen/qwen3-coder)
" qwen " : [
{ " id " : " qwen3-coder " , " label " : " Qwen3 Coder " } ,
{ " id " : " qwen3.6-plus " , " label " : " Qwen3.6 Plus " } ,
] ,
# xAI — prefix used in OpenRouter model IDs (x-ai/grok-4-20)
" x-ai " : [
2026-04-15 23:00:29 +00:00
{ " id " : " grok-4.20 " , " label " : " Grok 4.20 " } ,
2026-04-15 22:54:18 +00:00
] ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
}
2026-04-22 20:18:02 +00:00
_AMBIENT_GH_CLI_MARKERS = frozenset ( { " gh_cli " , " gh auth token " } )
def _is_ambient_gh_cli_entry ( source : str , label : str , key_source : str ) - > bool :
""" True when a credential-pool entry is a seeded gh-cli token rather than
one the user added explicitly . Filter these so Copilot doesn ' t appear in
the dropdown just because ` gh ` is installed on the system .
"""
return (
source . strip ( ) . lower ( ) in _AMBIENT_GH_CLI_MARKERS
or label . strip ( ) . lower ( ) == " gh auth token "
or key_source . strip ( ) . lower ( ) == " gh auth token "
)
def _format_ollama_label ( mid : str ) - > str :
""" Turn an Ollama model id (Ollama tag format) into a readable display label.
Examples : ' kimi-k2.5 ' → ' Kimi K2.5 ' , ' qwen3-vl:235b-instruct ' → ' Qwen3 VL (235B Instruct) '
"""
name_part , _ , variant = mid . partition ( " : " )
def _fmt ( s : str ) - > str :
tokens = s . replace ( " - " , " " ) . replace ( " _ " , " " ) . split ( )
out = [ ]
for t in tokens :
alpha_only = t . replace ( " . " , " " )
if alpha_only . isalpha ( ) and len ( t ) < = 3 :
out . append ( t . upper ( ) ) # short acronym: glm → GLM, vl → VL, gpt → GPT
elif alpha_only . isalnum ( ) and alpha_only and alpha_only [ 0 ] . isdigit ( ) :
out . append ( t . upper ( ) ) # size param: 235b → 235B, 1t → 1T
else :
out . append ( t [ 0 ] . upper ( ) + t [ 1 : ] if t else t ) # capitalize: kimi → Kimi
return " " . join ( out )
label = _fmt ( name_part )
if variant :
label + = f " ( { _fmt ( variant ) } ) "
return label
def _apply_provider_prefix (
raw_models : list [ dict ] ,
provider_id : str ,
active_provider : str | None ,
) - > list [ dict ] :
""" Return *raw_models* with @provider: prefixes applied when needed.
Prefixing is skipped when ( a ) the provider is already the active one , or
( b ) a model id already starts with ' @ ' or contains ' / ' ( already routable ) .
"""
_active = ( active_provider or " " ) . lower ( )
if not _active or provider_id == _active :
return list ( raw_models )
result = [ ]
for m in raw_models :
mid = m [ " id " ]
if mid . startswith ( " @ " ) or " / " in mid :
result . append ( { " id " : mid , " label " : m [ " label " ] } )
else :
result . append ( { " id " : f " @ { provider_id } : { mid } " , " label " : m [ " label " ] } )
return result
2026-04-05 06:30:20 +00:00
def resolve_model_provider ( model_id : str ) - > tuple :
2026-04-06 21:10:26 +00:00
""" Resolve model name, provider, and base_url for AIAgent.
2026-04-02 05:56:34 +00:00
2026-04-06 21:10:26 +00:00
Model IDs from the dropdown can be in several formats :
2026-04-10 01:23:40 +00:00
- ' claude-sonnet-4.6 ' ( bare name , uses config default provider )
- ' anthropic/claude-sonnet-4.6 ' ( OpenRouter - style provider / model )
- ' @minimax:MiniMax-M2.7 ' ( explicit provider hint from dropdown )
2026-04-02 05:56:34 +00:00
2026-04-06 21:10:26 +00:00
The @provider : model format is used for models from non - default provider
groups in the dropdown , so we can route them through the correct provider
via resolve_runtime_provider ( requested = provider ) instead of the default .
2026-04-02 06:15:04 +00:00
2026-04-10 01:23:40 +00:00
Custom OpenAI - compatible endpoints are special : their model IDs often look
like provider / model ( for example ` ` google / gemma - 4 - 26 b - a4b ` ` ) , which would be
mistaken for an OpenRouter model if we only looked at the slash . To avoid
that , first check whether the selected model matches an entry in
config . yaml - > custom_providers and route it through that named custom
provider .
2026-04-02 06:15:04 +00:00
Returns ( model , provider , base_url ) where provider and base_url may be None .
2026-04-02 05:56:34 +00:00
"""
config_provider = None
2026-04-02 06:15:04 +00:00
config_base_url = None
2026-04-12 07:11:41 +00:00
model_cfg = cfg . get ( " model " , { } )
2026-04-02 05:56:34 +00:00
if isinstance ( model_cfg , dict ) :
2026-04-12 07:11:41 +00:00
config_provider = model_cfg . get ( " provider " )
config_base_url = model_cfg . get ( " base_url " )
2026-04-02 05:56:34 +00:00
2026-04-12 07:11:41 +00:00
model_id = ( model_id or " " ) . strip ( )
2026-04-02 05:56:34 +00:00
if not model_id :
2026-04-02 06:15:04 +00:00
return model_id , config_provider , config_base_url
2026-04-02 05:56:34 +00:00
2026-04-10 01:23:40 +00:00
# Custom providers declared in config.yaml should win over slash-based
# OpenRouter heuristics. Their model IDs commonly contain '/' too.
2026-04-12 07:11:41 +00:00
custom_providers = cfg . get ( " custom_providers " , [ ] )
2026-04-10 01:23:40 +00:00
if isinstance ( custom_providers , list ) :
for entry in custom_providers :
if not isinstance ( entry , dict ) :
continue
2026-04-12 07:11:41 +00:00
entry_model = ( entry . get ( " model " ) or " " ) . strip ( )
entry_name = ( entry . get ( " name " ) or " " ) . strip ( )
entry_base_url = ( entry . get ( " base_url " ) or " " ) . strip ( )
2026-04-10 01:23:40 +00:00
if entry_model and entry_name and model_id == entry_model :
2026-04-12 07:11:41 +00:00
provider_hint = " custom: " + entry_name . lower ( ) . replace ( " " , " - " )
2026-04-10 01:23:40 +00:00
return model_id , provider_hint , entry_base_url or None
2026-04-06 21:10:26 +00:00
# @provider:model format — explicit provider hint from the dropdown.
# Route through that provider directly (resolve_runtime_provider will
# resolve credentials in streaming.py).
2026-04-12 07:11:41 +00:00
if model_id . startswith ( " @ " ) and " : " in model_id :
provider_hint , bare_model = model_id [ 1 : ] . split ( " : " , 1 )
2026-04-06 21:10:26 +00:00
return bare_model , provider_hint , None
2026-04-12 07:11:41 +00:00
if " / " in model_id :
prefix , bare = model_id . split ( " / " , 1 )
2026-04-05 20:58:37 +00:00
# OpenRouter always needs the full provider/model path (e.g. openrouter/free,
# anthropic/claude-sonnet-4.6). Never strip the prefix for OpenRouter.
2026-04-12 07:11:41 +00:00
if config_provider == " openrouter " :
return model_id , " openrouter " , config_base_url
fix: cross-provider model pick causes Connection lost on non-OpenRouter profiles
Root cause: resolve_model_provider() had a branch:
if config_provider and config_provider != 'openrouter' and prefix in _PROVIDER_MODELS:
return bare, prefix, None
When Camanji profile (config_provider='anthropic') picked openai/gpt-5.4-mini
from the OpenRouter dropdown, prefix='openai' matched _PROVIDER_MODELS and
config_provider was not 'openrouter', so it returned ('gpt-5.4-mini', 'openai', None).
The agent then demanded OPENAI_API_KEY directly -- not found -- RuntimeError --
stream crashed -- 'Connection lost'.
Fix: if prefix != config_provider (cross-provider selection), always route through
openrouter with the full provider/model string. Only strip the prefix and call a
direct provider API when the config_provider EXACTLY matches the model prefix.
Cases verified:
openrouter + openai/gpt-5.4-mini -> (openai/gpt-5.4-mini, openrouter) ✓
anthropic + openai/gpt-5.4-mini -> (openai/gpt-5.4-mini, openrouter) ✓ FIXED
anthropic + anthropic/claude-... -> (claude-..., anthropic) ✓
anthropic + claude-sonnet-4-6 bare -> (claude-sonnet-4-6, anthropic) ✓
openrouter + anthropic/claude-... -> (anthropic/claude-..., openrouter) ✓
Tests: 426 passed, 0 failed.
2026-04-03 20:23:25 +00:00
# If prefix matches config provider exactly, strip it and use that provider directly.
# e.g. config=anthropic, model=anthropic/claude-... → bare name to anthropic API
2026-04-02 05:56:34 +00:00
if config_provider and prefix == config_provider :
2026-04-02 06:15:04 +00:00
return bare , config_provider , config_base_url
2026-04-23 17:44:10 +00:00
# Portal providers (Nous, OpenCode) serve models from multiple upstream
# namespaces — check them BEFORE the config_base_url branch so that a
# Nous user whose config.yaml also has a base_url doesn't accidentally
# fall into the prefix-stripping path (#894: minimax/minimax-m2.7 → bare
# name sent to Nous → 404 because Nous requires the full namespace path).
_PORTAL_PROVIDERS = { " nous " , " opencode-zen " , " opencode-go " }
if config_provider in _PORTAL_PROVIDERS :
return model_id , config_provider , config_base_url
v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)
When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.
Fixes #230
* test: mobile layout regression suite — 14 tests for every QA run (#254)
Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.
* feat: /skills slash command lists and filters available Hermes skills (#257)
Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.
Fixes #248
* feat: shared app dialogs replace native confirm()/prompt() calls (#251)
Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.
Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.
Extracted from PR #242.
* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)
Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.
Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.
Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.
Fixes #247 Fixes #246
* feat: session ⋯ action dropdown replaces per-row buttons (#252)
Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.
5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.
Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.
Extracted from PR #242.
* docs: v0.47.0 release notes, bump version, update test counts (645)
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 19:19:12 +00:00
# If a custom endpoint base_url is configured, don't reroute through OpenRouter
# just because the model name contains a slash (e.g. google/gemma-4-26b-a4b).
# The user has explicitly pointed at a base_url, so trust their routing config.
if config_base_url :
2026-04-15 22:11:15 +00:00
# Only strip the provider prefix when it's a known provider namespace
# (e.g. "openai/gpt-5.4" → "gpt-5.4" for a custom OpenAI-compatible proxy).
# Unknown prefixes (e.g. "zai-org/GLM-5.1" on DeepInfra) are intrinsic to
# the model ID and must be preserved — stripping them causes model_not_found.
if prefix in _PROVIDER_MODELS :
return bare , config_provider , config_base_url
# Unknown prefix (not a named provider) — pass full model_id through.
return model_id , config_provider , config_base_url
2026-04-22 23:05:27 +00:00
fix: cross-provider model pick causes Connection lost on non-OpenRouter profiles
Root cause: resolve_model_provider() had a branch:
if config_provider and config_provider != 'openrouter' and prefix in _PROVIDER_MODELS:
return bare, prefix, None
When Camanji profile (config_provider='anthropic') picked openai/gpt-5.4-mini
from the OpenRouter dropdown, prefix='openai' matched _PROVIDER_MODELS and
config_provider was not 'openrouter', so it returned ('gpt-5.4-mini', 'openai', None).
The agent then demanded OPENAI_API_KEY directly -- not found -- RuntimeError --
stream crashed -- 'Connection lost'.
Fix: if prefix != config_provider (cross-provider selection), always route through
openrouter with the full provider/model string. Only strip the prefix and call a
direct provider API when the config_provider EXACTLY matches the model prefix.
Cases verified:
openrouter + openai/gpt-5.4-mini -> (openai/gpt-5.4-mini, openrouter) ✓
anthropic + openai/gpt-5.4-mini -> (openai/gpt-5.4-mini, openrouter) ✓ FIXED
anthropic + anthropic/claude-... -> (claude-..., anthropic) ✓
anthropic + claude-sonnet-4-6 bare -> (claude-sonnet-4-6, anthropic) ✓
openrouter + anthropic/claude-... -> (anthropic/claude-..., openrouter) ✓
Tests: 426 passed, 0 failed.
2026-04-03 20:23:25 +00:00
# If prefix does NOT match config provider, the user picked a cross-provider model
# from the OpenRouter dropdown (e.g. config=anthropic but picked openai/gpt-5.4-mini).
# In this case always route through openrouter with the full provider/model string.
if prefix in _PROVIDER_MODELS and prefix != config_provider :
2026-04-12 07:11:41 +00:00
return model_id , " openrouter " , None
2026-04-02 05:56:34 +00:00
2026-04-02 06:15:04 +00:00
return model_id , config_provider , config_base_url
2026-04-02 05:56:34 +00:00
2026-04-20 22:12:01 +00:00
def get_effective_default_model ( config_data : dict | None = None ) - > str :
""" Resolve the effective Hermes default model from config, then env overrides. """
active_cfg = config_data if config_data is not None else cfg
default_model = DEFAULT_MODEL
model_cfg = active_cfg . get ( " model " , { } )
if isinstance ( model_cfg , str ) :
default_model = model_cfg . strip ( )
elif isinstance ( model_cfg , dict ) :
cfg_default = str ( model_cfg . get ( " default " ) or " " ) . strip ( )
if cfg_default :
default_model = cfg_default
env_model = (
os . getenv ( " HERMES_MODEL " ) or os . getenv ( " OPENAI_MODEL " ) or os . getenv ( " LLM_MODEL " )
)
if env_model :
default_model = env_model . strip ( )
return default_model
2026-04-21 22:26:52 +00:00
# ── Reasoning config (CLI parity for /reasoning) ─────────────────────────────
# Mirrors hermes_constants.parse_reasoning_effort so WebUI can validate without
# importing from the agent tree (which may not be installed). Any drift here
# will show up in the shared test suite since both sides accept the same set.
VALID_REASONING_EFFORTS = ( " minimal " , " low " , " medium " , " high " , " xhigh " )
def parse_reasoning_effort ( effort ) :
""" Parse an effort level into the dict the agent expects.
Returns None when * effort * is empty or unrecognised ( caller interprets as
" use default " ) , ` ` { " enabled " : False } ` ` for ` ` " none " ` ` , and
` ` { " enabled " : True , " effort " : < level > } ` ` for any of
` ` VALID_REASONING_EFFORTS ` ` .
"""
if not effort or not str ( effort ) . strip ( ) :
return None
eff = str ( effort ) . strip ( ) . lower ( )
if eff == " none " :
return { " enabled " : False }
if eff in VALID_REASONING_EFFORTS :
return { " enabled " : True , " effort " : eff }
return None
def get_reasoning_status ( ) - > dict :
""" Return current reasoning configuration from the active profile ' s
config . yaml — the same source of truth the CLI reads from .
Keys :
- show_reasoning : bool — from ` ` display . show_reasoning ` ` ( default True )
- reasoning_effort : str — from ` ` agent . reasoning_effort ` ` ( ' ' = default )
"""
config_data = _load_yaml_config_file ( _get_config_path ( ) )
display_cfg = config_data . get ( " display " ) or { }
agent_cfg = config_data . get ( " agent " ) or { }
show_raw = display_cfg . get ( " show_reasoning " ) if isinstance ( display_cfg , dict ) else None
effort_raw = agent_cfg . get ( " reasoning_effort " ) if isinstance ( agent_cfg , dict ) else None
return {
# Match CLI default (True if unset in config.yaml)
" show_reasoning " : bool ( show_raw ) if isinstance ( show_raw , bool ) else True ,
" reasoning_effort " : str ( effort_raw or " " ) . strip ( ) . lower ( ) ,
}
def set_reasoning_display ( show : bool ) - > dict :
""" Persist ``display.show_reasoning`` to the active profile ' s config.yaml.
Mirrors CLI ` ` / reasoning show | hide ` ` : writes the same key that the CLI
writes , so the preference is shared across the WebUI and the terminal
REPL for the same profile .
"""
config_path = _get_config_path ( )
with _cfg_lock :
config_data = _load_yaml_config_file ( config_path )
display_cfg = config_data . get ( " display " )
if not isinstance ( display_cfg , dict ) :
display_cfg = { }
display_cfg [ " show_reasoning " ] = bool ( show )
config_data [ " display " ] = display_cfg
_save_yaml_config_file ( config_path , config_data )
reload_config ( )
return get_reasoning_status ( )
def set_reasoning_effort ( effort : str ) - > dict :
""" Persist ``agent.reasoning_effort`` to the active profile ' s config.yaml.
Mirrors CLI ` ` / reasoning < level > ` ` : same key , same valid values
( ` ` none ` ` | ` ` minimal ` ` | ` ` low ` ` | ` ` medium ` ` | ` ` high ` ` | ` ` xhigh ` ` ) .
Raises ` ` ValueError ` ` on an unrecognised level so callers can return 400.
"""
raw = str ( effort or " " ) . strip ( ) . lower ( )
if not raw :
raise ValueError ( " effort is required " )
if raw != " none " and raw not in VALID_REASONING_EFFORTS :
raise ValueError (
f " Unknown reasoning effort ' { effort } ' . "
f " Valid: none, { ' , ' . join ( VALID_REASONING_EFFORTS ) } . "
)
config_path = _get_config_path ( )
with _cfg_lock :
config_data = _load_yaml_config_file ( config_path )
agent_cfg = config_data . get ( " agent " )
if not isinstance ( agent_cfg , dict ) :
agent_cfg = { }
agent_cfg [ " reasoning_effort " ] = raw
config_data [ " agent " ] = agent_cfg
_save_yaml_config_file ( config_path , config_data )
reload_config ( )
return get_reasoning_status ( )
2026-04-20 22:12:01 +00:00
def set_hermes_default_model ( model_id : str ) - > dict :
""" Persist the Hermes default model in config.yaml and reload runtime config. """
selected_model = str ( model_id or " " ) . strip ( )
if not selected_model :
raise ValueError ( " model is required " )
config_path = _get_config_path ( )
# Hold _cfg_lock only around the read-modify-write of the YAML file.
# reload_config() acquires _cfg_lock internally (it's not reentrant) so
# it must be called AFTER releasing the lock to avoid deadlock.
with _cfg_lock :
config_data = _load_yaml_config_file ( config_path )
model_cfg = config_data . get ( " model " , { } )
if not isinstance ( model_cfg , dict ) :
model_cfg = { }
previous_provider = str ( model_cfg . get ( " provider " ) or " " ) . strip ( )
resolved_model , resolved_provider , resolved_base_url = resolve_model_provider (
selected_model
)
2026-04-23 17:44:10 +00:00
# Persist the resolved bare/slash form, NOT the `@provider:` prefix. The
# prefix is a WebUI-internal routing hint that the hermes-agent CLI does
# not understand — if we wrote `@nous:anthropic/claude-opus-4.6` to
# config.yaml, a user who ran `hermes` in the terminal right after
# saving via WebUI would have the agent send that literal string to the
# Nous API, which would reject it (Nous expects `anthropic/claude-opus-4.6`,
# not the prefixed form). The Settings picker handles the resulting
# CLI-shaped bare form via `_applyModelToDropdown()`'s normalising
# matcher — see `static/panels.js` (#895).
2026-04-20 22:12:01 +00:00
persisted_model = str ( resolved_model or selected_model ) . strip ( )
persisted_provider = str ( resolved_provider or previous_provider or " " ) . strip ( )
model_cfg [ " default " ] = persisted_model
if persisted_provider :
model_cfg [ " provider " ] = persisted_provider
if resolved_base_url :
model_cfg [ " base_url " ] = str ( resolved_base_url ) . strip ( ) . rstrip ( " / " )
elif persisted_provider != previous_provider :
if persisted_provider == " openai " :
model_cfg [ " base_url " ] = " https://api.openai.com/v1 "
elif not persisted_provider . startswith ( " custom: " ) :
model_cfg . pop ( " base_url " , None )
config_data [ " model " ] = model_cfg
_save_yaml_config_file ( config_path , config_data )
# Reload outside the lock — reload_config() acquires _cfg_lock itself.
reload_config ( )
2026-04-23 17:44:10 +00:00
# Invalidate the TTL cache so the next /api/models call returns fresh data
# with the new default model. Do NOT call get_available_models() here —
# it triggers a live provider fetch (up to 8s) that blocks the HTTP response
# to the browser, causing a visible freeze on every Settings save (#895).
2026-04-21 02:32:33 +00:00
invalidate_models_cache ( )
2026-04-23 17:44:10 +00:00
return { " ok " : True , " model " : persisted_model }
2026-04-20 22:12:01 +00:00
2026-04-21 00:33:03 +00:00
# ── TTL cache for get_available_models() ─────────────────────────────────────
_available_models_cache : dict | None = None
_available_models_cache_ts : float = 0.0
_AVAILABLE_MODELS_CACHE_TTL : float = 60.0 # seconds — refresh at most once per minute
_available_models_cache_lock = threading . Lock ( )
def invalidate_models_cache ( ) :
""" Force the TTL cache for get_available_models() to be cleared.
Call this after modifying config . cfg in - memory ( e . g . in tests ) so
the next call to get_available_models ( ) picks up the changes rather
than returning a stale cached result .
"""
global _available_models_cache , _available_models_cache_ts
with _available_models_cache_lock :
_available_models_cache = None
_available_models_cache_ts = 0.0
2026-04-23 21:41:06 +00:00
def _get_label_for_model ( model_id : str , existing_groups : list ) - > str :
""" Return a human-friendly label for *model_id*.
Resolution order :
1. If the model already appears in * existing_groups * with a label , use it .
2. Strip @provider : prefix and namespace prefix , then title - case .
This ensures the injected default model entry in the dropdown always shows
the same label as the live - fetched or static - catalog version , rather than
the raw lowercase ID string ( #909).
"""
# Strip @provider: prefix for lookup
lookup_id = model_id
if lookup_id . startswith ( " @ " ) and " : " in lookup_id :
lookup_id = lookup_id . split ( " : " , 1 ) [ 1 ]
# Check existing groups for a matching label
_norm = lambda s : ( s . split ( " / " , 1 ) [ - 1 ] if " / " in s else s ) . replace ( " - " , " . " ) . lower ( )
norm_lookup = _norm ( lookup_id )
for g in existing_groups :
for m in g . get ( " models " , [ ] ) :
if m . get ( " label " ) and _norm ( str ( m . get ( " id " , " " ) ) ) == norm_lookup :
return m [ " label " ]
# Fall back: capitalize each hyphen-separated word, preserve dots in version numbers.
# The catalog lookup above handles well-known models; this only fires for unlisted IDs.
bare = lookup_id . split ( " / " ) [ - 1 ] if " / " in lookup_id else lookup_id
return " " . join (
w . upper ( ) if ( len ( w ) < = 3 and w . replace ( " . " , " " ) . isalnum ( ) and not w . isdigit ( ) ) else w . capitalize ( )
for w in bare . replace ( " _ " , " - " ) . split ( " - " )
)
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
def get_available_models ( ) - > dict :
"""
Return available models grouped by provider .
Discovery order :
1. Read config . yaml ' model ' section for active provider info
2. Check for known API keys in env or ~ / . hermes / . env
2026-04-02 06:32:49 +00:00
3. Fetch models from custom endpoint if base_url is configured
4. Fall back to hardcoded model list ( OpenRouter - style )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
Returns : {
' active_provider ' : str | None ,
' default_model ' : str ,
' groups ' : [ { ' provider ' : str , ' models ' : [ { ' id ' : str , ' label ' : str } ] } ]
}
"""
2026-04-16 18:55:18 +00:00
# Reload config from disk if config.yaml has changed since last load.
# This ensures CLI model changes are picked up on page refresh without
# a server restart, while avoiding clearing in-memory mocks during tests. (#585)
2026-04-21 00:33:03 +00:00
# Must run BEFORE the TTL check so config edits within the 60s window are visible.
global _available_models_cache , _available_models_cache_ts
with _available_models_cache_lock :
try :
_current_mtime = Path ( _get_config_path ( ) ) . stat ( ) . st_mtime
except OSError :
_current_mtime = 0.0
# Note: env-var changes (e.g. API key rotation) are not detected by mtime;
# cache will be stale for up to TTL seconds in that case.
if _current_mtime != _cfg_mtime :
reload_config ( )
# Config changed — force cache invalidation
_available_models_cache = None
_available_models_cache_ts = 0.0
# Serve from TTL cache if fresh.
now = time . monotonic ( )
if _available_models_cache is not None and ( now - _available_models_cache_ts ) < _AVAILABLE_MODELS_CACHE_TTL :
return copy . deepcopy ( _available_models_cache )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
active_provider = None
2026-04-20 22:12:01 +00:00
default_model = get_effective_default_model ( cfg )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
groups = [ ]
# 1. Read config.yaml model section
2026-04-12 07:11:41 +00:00
cfg_base_url = " " # must be defined before conditional blocks (#117)
model_cfg = cfg . get ( " model " , { } )
cfg_base_url = " "
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
if isinstance ( model_cfg , str ) :
2026-04-20 22:12:01 +00:00
pass # default_model already set by get_effective_default_model
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
elif isinstance ( model_cfg , dict ) :
2026-04-12 07:11:41 +00:00
active_provider = model_cfg . get ( " provider " )
cfg_default = model_cfg . get ( " default " , " " )
cfg_base_url = model_cfg . get ( " base_url " , " " )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
if cfg_default :
default_model = cfg_default
2026-04-22 00:24:54 +00:00
# Normalize active_provider to its canonical key so it matches the
# _PROVIDER_MODELS lookup below (e.g. 'z.ai' -> 'zai', 'x.ai' -> 'xai',
# 'google' -> 'gemini'). Works even when hermes_cli is not on sys.path
# because the WebUI ships its own _PROVIDER_ALIASES table.
if active_provider :
active_provider = _resolve_provider_alias ( active_provider )
2026-04-22 20:18:02 +00:00
# 2. Read auth store (active_provider fallback + credential_pool inspection)
auth_store = { }
try :
from api . profiles import get_active_hermes_home as _gah
2026-04-12 07:11:41 +00:00
2026-04-22 20:18:02 +00:00
auth_store_path = _gah ( ) / " auth.json "
except ImportError :
auth_store_path = HOME / " .hermes " / " auth.json "
if auth_store_path . exists ( ) :
try :
import json as _j
auth_store = _j . loads ( auth_store_path . read_text ( encoding = " utf-8 " ) )
if not active_provider :
# Re-run alias resolution: auth.json may store an aliased name
# (e.g. 'google', 'z.ai') that the prefixing logic compares
# against canonical pids.
active_provider = _resolve_provider_alias ( auth_store . get ( " active_provider " ) )
except Exception :
logger . debug ( " Failed to load auth store from %s " , auth_store_path )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-04-22 20:18:02 +00:00
# 3. Detect available providers.
2026-04-06 21:29:06 +00:00
# Primary: ask hermes-agent's auth layer — the authoritative source. It checks
# auth.json, credential pools, and env vars the same way the agent does at runtime,
# so the dropdown reflects exactly what the user has configured.
# Fallback: scan raw API key env vars (matches old behaviour if hermes not available).
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
detected_providers = set ( )
if active_provider :
detected_providers . add ( active_provider )
2026-04-22 20:18:02 +00:00
# Include providers that have usable credential-pool entries even when no
# process env var is present (e.g. service launched without shell env).
# Primary: delegate to upstream credential_pool.load_pool() so suppression
# and seeding/pruning rules live in one place.
# Fallback: manual field inspection when the upstream module is unavailable.
try :
_pool = auth_store . get ( " credential_pool " , { } ) if isinstance ( auth_store , dict ) else { }
if isinstance ( _pool , dict ) and _pool :
try :
from agent . credential_pool import load_pool as _load_pool
# load_pool() does NOT suppress ambient gh-cli tokens — filter
# them here so Copilot doesn't reappear just because the agent
# seeded 'gh auth token' into the pool.
for _pid in list ( _pool . keys ( ) ) :
try :
_canonical_pid = _resolve_provider_alias ( str ( _pid ) )
_all_entries = _load_pool ( _pid ) . entries ( )
_explicit = [
e for e in _all_entries
if not _is_ambient_gh_cli_entry (
str ( getattr ( e , " source " , " " ) or " " ) ,
str ( getattr ( e , " label " , " " ) or " " ) ,
str ( getattr ( e , " key_source " , " " ) or " " ) ,
)
]
if _explicit :
detected_providers . add ( _canonical_pid )
except Exception :
logger . debug ( " credential_pool.load_pool( %s ) failed " , _pid )
except ImportError :
# Fallback: inspect raw entry fields for suppression signals.
for _pid , _entries in _pool . items ( ) :
if not isinstance ( _entries , list ) or len ( _entries ) == 0 :
continue
_has_explicit_cred = any (
isinstance ( _entry , dict )
and not _is_ambient_gh_cli_entry (
str ( _entry . get ( " source " , " " ) or " " ) ,
str ( _entry . get ( " label " , " " ) or " " ) ,
str ( _entry . get ( " key_source " , " " ) or " " ) ,
)
for _entry in _entries
)
if _has_explicit_cred :
detected_providers . add ( _resolve_provider_alias ( str ( _pid ) ) )
except Exception :
logger . debug ( " Failed to inspect credential_pool from auth store " )
2026-04-06 21:39:19 +00:00
all_env : dict = { } # profile .env keys — populated below, used by custom endpoint auth
2026-04-06 21:29:06 +00:00
_hermes_auth_used = False
try :
from hermes_cli . models import list_available_providers as _lap
2026-04-06 21:35:30 +00:00
from hermes_cli . auth import get_auth_status as _gas
2026-04-12 07:11:41 +00:00
2026-04-06 21:29:06 +00:00
for _p in _lap ( ) :
2026-04-12 07:11:41 +00:00
if not _p . get ( " authenticated " ) :
2026-04-06 21:35:30 +00:00
continue
# Exclude providers whose credential came from an ambient token
# (e.g. 'gh auth token' for Copilot on a machine with gh CLI auth).
# Only include providers with an explicit, dedicated credential.
try :
2026-04-12 07:11:41 +00:00
_src = _gas ( _p [ " id " ] ) . get ( " key_source " , " " )
if _src == " gh auth token " :
2026-04-06 21:35:30 +00:00
continue
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Failed to get key source for provider %s " , _p . get ( " id " , " unknown " ) )
2026-04-12 07:11:41 +00:00
detected_providers . add ( _p [ " id " ] )
2026-04-06 21:29:06 +00:00
_hermes_auth_used = True
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Failed to detect auth providers from hermes " )
2026-04-06 21:29:06 +00:00
if not _hermes_auth_used :
# Fallback: scan .env and os.environ for known API key variables
try :
from api . profiles import get_active_hermes_home as _gah2
2026-04-12 07:11:41 +00:00
hermes_env_path = _gah2 ( ) / " .env "
2026-04-06 21:29:06 +00:00
except ImportError :
2026-04-12 07:11:41 +00:00
hermes_env_path = HOME / " .hermes " / " .env "
2026-04-06 21:29:06 +00:00
env_keys = { }
if hermes_env_path . exists ( ) :
try :
2026-04-19 04:22:28 +00:00
for line in hermes_env_path . read_text ( encoding = " utf-8 " ) . splitlines ( ) :
2026-04-06 21:29:06 +00:00
line = line . strip ( )
2026-04-12 07:11:41 +00:00
if line and not line . startswith ( " # " ) and " = " in line :
k , v = line . split ( " = " , 1 )
2026-04-06 21:29:06 +00:00
env_keys [ k . strip ( ) ] = v . strip ( ) . strip ( ' " ' ) . strip ( " ' " )
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Failed to parse hermes env file " )
2026-04-06 21:29:06 +00:00
all_env = { * * env_keys }
2026-04-12 07:11:41 +00:00
for k in (
" ANTHROPIC_API_KEY " ,
" OPENAI_API_KEY " ,
" OPENROUTER_API_KEY " ,
" GOOGLE_API_KEY " ,
2026-04-18 17:09:59 +00:00
" GEMINI_API_KEY " ,
2026-04-12 07:11:41 +00:00
" GLM_API_KEY " ,
" KIMI_API_KEY " ,
" DEEPSEEK_API_KEY " ,
2026-04-14 01:46:11 +00:00
" OPENCODE_ZEN_API_KEY " ,
" OPENCODE_GO_API_KEY " ,
2026-04-18 07:18:20 +00:00
" MINIMAX_API_KEY " ,
" MINIMAX_CN_API_KEY " ,
2026-04-12 07:11:41 +00:00
) :
2026-04-06 21:29:06 +00:00
val = os . getenv ( k )
if val :
all_env [ k ] = val
2026-04-12 07:11:41 +00:00
if all_env . get ( " ANTHROPIC_API_KEY " ) :
detected_providers . add ( " anthropic " )
if all_env . get ( " OPENAI_API_KEY " ) :
detected_providers . add ( " openai " )
if all_env . get ( " OPENROUTER_API_KEY " ) :
detected_providers . add ( " openrouter " )
if all_env . get ( " GOOGLE_API_KEY " ) :
detected_providers . add ( " google " )
2026-04-18 17:09:59 +00:00
if all_env . get ( " GEMINI_API_KEY " ) :
detected_providers . add ( " gemini " )
2026-04-12 07:11:41 +00:00
if all_env . get ( " GLM_API_KEY " ) :
detected_providers . add ( " zai " )
if all_env . get ( " KIMI_API_KEY " ) :
detected_providers . add ( " kimi-coding " )
if all_env . get ( " MINIMAX_API_KEY " ) or all_env . get ( " MINIMAX_CN_API_KEY " ) :
detected_providers . add ( " minimax " )
if all_env . get ( " DEEPSEEK_API_KEY " ) :
detected_providers . add ( " deepseek " )
2026-04-14 01:46:11 +00:00
if all_env . get ( " OPENCODE_ZEN_API_KEY " ) :
detected_providers . add ( " opencode-zen " )
if all_env . get ( " OPENCODE_GO_API_KEY " ) :
detected_providers . add ( " opencode-go " )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-04-22 20:18:02 +00:00
# 4. Fetch models from custom endpoint if base_url is configured
2026-04-02 16:15:41 +00:00
auto_detected_models = [ ]
2026-04-02 06:32:49 +00:00
if cfg_base_url :
try :
import ipaddress
2026-04-02 07:18:41 +00:00
import urllib . request
2026-04-02 16:15:41 +00:00
# Normalize the base_url and build models endpoint
2026-04-02 06:32:49 +00:00
base_url = cfg_base_url . strip ( )
2026-04-12 07:11:41 +00:00
if base_url . endswith ( " /v1 " ) :
endpoint_url = base_url + " /models " # /v1/models
2026-04-02 06:32:49 +00:00
else :
2026-04-12 07:11:41 +00:00
endpoint_url = base_url . rstrip ( " / " ) + " /v1/models "
2026-04-02 16:15:41 +00:00
2026-04-02 06:32:49 +00:00
# Detect provider from base_url
2026-04-12 07:11:41 +00:00
provider = " custom "
parsed = urlparse ( base_url if " :// " in base_url else f " http:// { base_url } " )
2026-04-02 16:15:41 +00:00
host = ( parsed . netloc or parsed . path ) . lower ( )
2026-04-02 06:32:49 +00:00
if parsed . hostname :
try :
addr = ipaddress . ip_address ( parsed . hostname )
if addr . is_private or addr . is_loopback or addr . is_link_local :
2026-04-12 07:11:41 +00:00
if (
" ollama " in host
or " 127.0.0.1 " in host
or " localhost " in host
) :
provider = " ollama "
elif " lmstudio " in host or " lm-studio " in host :
provider = " lmstudio "
2026-04-02 07:01:35 +00:00
else :
2026-04-12 07:11:41 +00:00
provider = " local "
2026-04-02 06:32:49 +00:00
except ValueError :
pass
2026-04-02 16:15:41 +00:00
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
# Resolve API key for the custom / OpenAI-compatible endpoint.
# Priority:
# 1. model.api_key in config.yaml
# 2. provider-specific providers.<active>.api_key / providers.custom.api_key
# 3. env/.env fallbacks
2026-04-02 06:32:49 +00:00
headers = { }
2026-04-12 07:11:41 +00:00
api_key = " "
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
if isinstance ( model_cfg , dict ) :
2026-04-12 07:11:41 +00:00
api_key = ( model_cfg . get ( " api_key " ) or " " ) . strip ( )
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
if not api_key :
2026-04-12 07:11:41 +00:00
providers_cfg = cfg . get ( " providers " , { } )
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
if isinstance ( providers_cfg , dict ) :
2026-04-12 07:11:41 +00:00
for provider_key in filter ( None , [ active_provider , " custom " ] ) :
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
provider_cfg = providers_cfg . get ( provider_key , { } )
if isinstance ( provider_cfg , dict ) :
2026-04-12 07:11:41 +00:00
api_key = ( provider_cfg . get ( " api_key " ) or " " ) . strip ( )
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
if api_key :
break
if not api_key :
2026-04-12 07:11:41 +00:00
api_key_vars = (
" HERMES_API_KEY " ,
" HERMES_OPENAI_API_KEY " ,
" OPENAI_API_KEY " ,
" LOCAL_API_KEY " ,
" OPENROUTER_API_KEY " ,
" API_KEY " ,
)
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
for key in api_key_vars :
2026-04-12 07:11:41 +00:00
api_key = ( all_env . get ( key ) or os . getenv ( key ) or " " ) . strip ( )
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
if api_key :
break
if api_key :
2026-04-12 07:11:41 +00:00
headers [ " Authorization " ] = f " Bearer { api_key } "
2026-04-02 16:15:41 +00:00
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
# Fetch model list from endpoint (with SSRF protection)
import socket
2026-04-12 07:11:41 +00:00
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
# Resolve hostname and check against private IPs after DNS lookup
2026-04-12 07:11:41 +00:00
parsed_url = urlparse (
endpoint_url if " :// " in endpoint_url else f " http:// { endpoint_url } "
)
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
# Validate URL scheme to prevent file:// and other dangerous schemes
if parsed_url . scheme not in ( " " , " http " , " https " ) :
raise ValueError ( f " Invalid URL scheme: { parsed_url . scheme } " )
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
if parsed_url . hostname :
try :
resolved_ips = socket . getaddrinfo ( parsed_url . hostname , None )
for _ , _ , _ , _ , addr in resolved_ips :
addr_obj = ipaddress . ip_address ( addr [ 0 ] )
2026-04-12 07:11:41 +00:00
if (
addr_obj . is_private
or addr_obj . is_loopback
or addr_obj . is_link_local
) :
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
# Allow known local providers (ollama, lmstudio)
2026-04-12 07:11:41 +00:00
is_known_local = any (
k in ( parsed_url . hostname or " " ) . lower ( )
for k in (
" ollama " ,
" localhost " ,
" 127.0.0.1 " ,
" lmstudio " ,
" lm-studio " ,
)
)
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
if not is_known_local :
2026-04-12 07:11:41 +00:00
raise ValueError (
f " SSRF: resolved hostname to private IP { addr [ 0 ] } "
)
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
except socket . gaierror :
pass # DNS resolution failed -- let urllib handle it
2026-04-12 07:11:41 +00:00
req = urllib . request . Request ( endpoint_url , method = " GET " )
req . add_header ( " User-Agent " , " OpenAI/Python 1.0 " )
2026-04-02 16:15:41 +00:00
for k , v in headers . items ( ) :
req . add_header ( k , v )
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
with urllib . request . urlopen ( req , timeout = 10 ) as response : # nosec B310
2026-04-12 07:11:41 +00:00
data = json . loads ( response . read ( ) . decode ( " utf-8 " ) )
2026-04-02 16:15:41 +00:00
# Handle both OpenAI-compatible and llama.cpp response formats
models_list = [ ]
2026-04-12 07:11:41 +00:00
if " data " in data and isinstance ( data [ " data " ] , list ) :
models_list = data [ " data " ]
elif " models " in data and isinstance ( data [ " models " ] , list ) :
models_list = data [ " models " ]
2026-04-02 16:15:41 +00:00
for model in models_list :
if not isinstance ( model , dict ) :
continue
2026-04-12 07:11:41 +00:00
model_id = (
model . get ( " id " , " " )
or model . get ( " name " , " " )
or model . get ( " model " , " " )
)
model_name = model . get ( " name " , " " ) or model . get ( " model " , " " ) or model_id
2026-04-02 16:15:41 +00:00
if model_id and model_name :
2026-04-22 20:18:02 +00:00
label = _format_ollama_label ( model_id ) if provider in ( " ollama " , " ollama-cloud " ) else model_name
auto_detected_models . append ( { " id " : model_id , " label " : label } )
2026-04-02 16:15:41 +00:00
detected_providers . add ( provider . lower ( ) )
2026-04-02 18:06:50 +00:00
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Custom endpoint unreachable or misconfigured for provider: %s " , provider )
2026-04-02 06:32:49 +00:00
2026-04-06 21:39:19 +00:00
# 3b. Include models from custom_providers config entries.
# These are explicitly configured and should always appear even when the
# /v1/models endpoint is unreachable or returns a subset.
2026-04-16 01:09:39 +00:00
#
# Each entry may carry a `name` field (e.g. "Agent37"). When present we
# use it as the dropdown section header instead of the generic "Custom"
# label. Internally we key these providers as "custom:<slug>" so that
# multiple named custom providers can coexist as separate groups.
2026-04-12 07:11:41 +00:00
_custom_providers_cfg = cfg . get ( " custom_providers " , [ ] )
2026-04-16 01:09:39 +00:00
# Maps "custom:<slug>" -> (display_name, [model_dicts])
_named_custom_groups : dict = { }
2026-04-06 21:39:19 +00:00
if isinstance ( _custom_providers_cfg , list ) :
2026-04-12 07:11:41 +00:00
_seen_custom_ids = { m [ " id " ] for m in auto_detected_models }
2026-04-06 21:39:19 +00:00
for _cp in _custom_providers_cfg :
if not isinstance ( _cp , dict ) :
continue
2026-04-12 07:11:41 +00:00
_cp_model = _cp . get ( " model " , " " )
2026-04-16 01:09:39 +00:00
_cp_name = ( _cp . get ( " name " ) or " " ) . strip ( )
2026-04-06 21:39:19 +00:00
if _cp_model and _cp_model not in _seen_custom_ids :
2026-04-23 21:41:06 +00:00
_cp_label = _get_label_for_model ( _cp_model , [ ] )
2026-04-06 21:39:19 +00:00
_seen_custom_ids . add ( _cp_model )
2026-04-16 01:09:39 +00:00
if _cp_name :
# Named custom provider — own group keyed by slug
_slug = " custom: " + _cp_name . lower ( ) . replace ( " " , " - " )
if _slug not in _named_custom_groups :
_named_custom_groups [ _slug ] = ( _cp_name , [ ] )
detected_providers . add ( _slug )
_named_custom_groups [ _slug ] [ 1 ] . append (
{ " id " : _cp_model , " label " : _cp_label }
)
else :
# Unnamed — falls into the generic "Custom" bucket
auto_detected_models . append ( { " id " : _cp_model , " label " : _cp_label } )
detected_providers . add ( " custom " )
2026-04-06 21:39:19 +00:00
2026-04-10 01:33:24 +00:00
# If the user configured a real model.provider, the base_url belongs to
# THAT provider, not to a separate "Custom" group. hermes_cli reports
# 'custom' as authenticated whenever base_url is set, which would otherwise
# build a phantom "Custom" bucket next to the real provider's group. Drop
2026-04-15 07:42:12 +00:00
# it unless (a) the user explicitly chose 'custom' as their active provider,
# or (b) the user has custom_providers entries in config.yaml (those models
# were already added above and should still be shown).
_has_custom_providers = isinstance ( _custom_providers_cfg , list ) and len ( _custom_providers_cfg ) > 0
if active_provider and active_provider != " custom " and not _has_custom_providers :
2026-04-12 07:11:41 +00:00
detected_providers . discard ( " custom " )
2026-04-16 01:09:39 +00:00
# Also drop named custom slugs when active provider is a real named one
# and there are no custom_providers entries to show.
for _slug in list ( detected_providers ) :
if _slug . startswith ( " custom: " ) and not _has_custom_providers :
detected_providers . discard ( _slug )
elif active_provider == " custom " and _has_custom_providers :
# When the active provider is 'custom' and all custom_providers entries
# are named (i.e. every entry produced a "custom:<slug>" key), the bare
# "custom" bucket is empty noise — discard it so the dropdown only shows
# the named groups. We keep "custom" if there are unnamed entries (they
# were added to auto_detected_models and will render under the generic
# "Custom" header via the else branch in the group builder).
_has_unnamed = any (
isinstance ( _cp , dict ) and not ( _cp . get ( " name " ) or " " ) . strip ( )
for _cp in _custom_providers_cfg
)
if not _has_unnamed :
detected_providers . discard ( " custom " )
2026-04-10 01:33:24 +00:00
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# 5. Build model groups
if detected_providers :
for pid in sorted ( detected_providers ) :
2026-04-16 01:09:39 +00:00
if pid . startswith ( " custom: " ) and pid in _named_custom_groups :
# Named custom provider — use the stored display name and its own model list
_nc_display , _nc_models = _named_custom_groups [ pid ]
if _nc_models :
2026-04-22 00:24:54 +00:00
groups . append ( { " provider " : _nc_display , " provider_id " : pid , " models " : _nc_models } )
2026-04-16 01:09:39 +00:00
continue
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
provider_name = _PROVIDER_DISPLAY . get ( pid , pid . title ( ) )
2026-04-12 07:11:41 +00:00
if pid == " openrouter " :
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# OpenRouter uses provider/model format -- show the fallback list
2026-04-12 07:11:41 +00:00
groups . append (
{
" provider " : " OpenRouter " ,
2026-04-22 00:24:54 +00:00
" provider_id " : " openrouter " ,
2026-04-12 07:11:41 +00:00
" models " : [
{ " id " : m [ " id " ] , " label " : m [ " label " ] }
for m in _FALLBACK_MODELS
] ,
}
)
2026-04-22 20:18:02 +00:00
elif pid == " ollama-cloud " :
# Ollama Cloud list is dynamic; fetch via hermes_cli provider catalog.
# When the catalog is unavailable, skip the group rather than emit a
# speculative static list — matches the named-custom and unknown-provider
# branches below.
raw_models = [ ]
try :
from hermes_cli . models import provider_model_ids as _provider_model_ids
raw_models = [
{ " id " : mid , " label " : _format_ollama_label ( mid ) }
for mid in ( _provider_model_ids ( " ollama-cloud " ) or [ ] )
]
except Exception :
logger . warning ( " Failed to load Ollama Cloud models from hermes_cli " )
if raw_models :
models = _apply_provider_prefix ( raw_models , pid , active_provider )
groups . append (
{
" provider " : provider_name ,
" provider_id " : pid ,
" models " : models ,
}
)
2026-04-18 07:14:03 +00:00
elif pid in _PROVIDER_MODELS or pid in cfg . get ( " providers " , { } ) :
2026-04-06 21:10:26 +00:00
# For non-default providers, prefix model IDs with @provider:model
# so resolve_model_provider() routes through that specific provider
# via resolve_runtime_provider(requested=provider).
2026-04-06 18:05:44 +00:00
# The default provider's models keep bare names for direct API routing.
2026-04-18 07:14:03 +00:00
raw_models = _PROVIDER_MODELS . get ( pid , [ ] )
# Override or merge from config.yaml if user specified explicit models
provider_cfg = cfg . get ( " providers " , { } ) . get ( pid , { } )
if isinstance ( provider_cfg , dict ) and " models " in provider_cfg :
cfg_models = provider_cfg [ " models " ]
if isinstance ( cfg_models , dict ) :
# config format is usually models: { "gpt-5.4": { context_length: ... } }
raw_models = [ { " id " : k , " label " : k } for k in cfg_models . keys ( ) ]
elif isinstance ( cfg_models , list ) :
raw_models = [ { " id " : k , " label " : k } for k in cfg_models ]
2026-04-22 20:18:02 +00:00
models = _apply_provider_prefix ( raw_models , pid , active_provider )
2026-04-12 07:11:41 +00:00
groups . append (
{
" provider " : provider_name ,
2026-04-22 00:24:54 +00:00
" provider_id " : pid ,
2026-04-12 07:11:41 +00:00
" models " : models ,
}
)
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
else :
2026-04-02 16:15:41 +00:00
# Unknown provider -- use auto-detected models if available,
2026-04-18 15:26:25 +00:00
# otherwise skip it for the model dropdown. Do NOT inject the
# global default_model here: that would incorrectly imply the
# provider can serve the default model (e.g. Alibaba -> gpt-5.4-mini).
2026-04-02 16:15:41 +00:00
if auto_detected_models :
2026-04-12 07:11:41 +00:00
groups . append (
{
" provider " : provider_name ,
2026-04-22 00:24:54 +00:00
" provider_id " : pid ,
2026-04-12 07:11:41 +00:00
" models " : auto_detected_models ,
}
)
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
else :
2026-04-06 21:29:06 +00:00
# No providers detected. Show only the configured default model so the user
# can at least send messages with their current setting. Avoid showing a
# generic multi-provider list — those models wouldn't be routable anyway.
2026-04-18 06:46:43 +00:00
if default_model :
2026-04-23 21:41:06 +00:00
label = _get_label_for_model ( default_model , groups )
2026-04-18 06:46:43 +00:00
groups . append (
2026-04-22 00:24:54 +00:00
{ " provider " : " Default " , " provider_id " : " default " , " models " : [ { " id " : default_model , " label " : label } ] }
2026-04-18 06:46:43 +00:00
)
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-04-05 15:29:40 +00:00
# Ensure the user's configured default_model always appears in the dropdown.
# It may be missing if the model isn't in any hardcoded list (e.g. openrouter/free,
# a custom local model, or any model.default not in _FALLBACK_MODELS).
2026-04-12 21:45:39 +00:00
# Normalize before comparing: strip provider prefix and unify separators so
# 'anthropic/claude-opus-4.6' matches 'claude-opus-4.6' and 'claude-sonnet-4-6'
# matches 'claude-sonnet-4.6' (hermes-agent uses hyphens, webui uses dots).
2026-04-05 15:29:40 +00:00
if default_model :
2026-04-12 21:45:39 +00:00
_norm = lambda mid : ( mid . split ( " / " , 1 ) [ - 1 ] if " / " in mid else mid ) . replace ( " - " , " . " )
2026-04-12 07:11:41 +00:00
all_ids_norm = { _norm ( m [ " id " ] ) for g in groups for m in g . get ( " models " , [ ] ) }
2026-04-06 21:15:24 +00:00
if _norm ( default_model ) not in all_ids_norm :
2026-04-10 01:33:24 +00:00
# Determine which group to inject into. Compare against the
# provider's display name from _PROVIDER_DISPLAY rather than
# doing a substring match on active_provider — substring
# matching breaks on hyphenated provider IDs like 'openai-codex'
# vs display name 'OpenAI Codex' (hyphen vs. space), which
# silently falls through to groups[0] and lands the model in
# the wrong group.
2026-04-23 21:41:06 +00:00
label = _get_label_for_model ( default_model , groups )
2026-04-10 01:33:24 +00:00
target_display = (
2026-04-12 07:11:41 +00:00
_PROVIDER_DISPLAY . get ( active_provider , active_provider or " " ) . lower ( )
if active_provider
else " "
2026-04-10 01:33:24 +00:00
)
2026-04-05 15:29:40 +00:00
injected = False
for g in groups :
2026-04-12 07:11:41 +00:00
if target_display and g . get ( " provider " , " " ) . lower ( ) == target_display :
g [ " models " ] . insert ( 0 , { " id " : default_model , " label " : label } )
2026-04-05 15:29:40 +00:00
injected = True
break
if not injected and groups :
2026-04-18 15:26:25 +00:00
# Keep the default isolated rather than polluting the first
# detected provider group.
groups . append (
{
" provider " : " Default " ,
2026-04-22 00:24:54 +00:00
" provider_id " : " default " ,
2026-04-18 15:26:25 +00:00
" models " : [ { " id " : default_model , " label " : label } ] ,
}
)
2026-04-05 15:29:40 +00:00
elif not groups :
2026-04-12 07:11:41 +00:00
groups . append (
{
" provider " : active_provider or " Default " ,
2026-04-22 00:24:54 +00:00
" provider_id " : active_provider or " default " ,
2026-04-12 07:11:41 +00:00
" models " : [ { " id " : default_model , " label " : label } ] ,
}
)
2026-04-05 15:29:40 +00:00
2026-04-21 00:33:03 +00:00
result = {
2026-04-12 07:11:41 +00:00
" active_provider " : active_provider ,
" default_model " : default_model ,
" groups " : groups ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
}
2026-04-21 00:33:03 +00:00
# Cache the result for TTL seconds
with _available_models_cache_lock :
_available_models_cache = result
_available_models_cache_ts = time . monotonic ( )
return copy . deepcopy ( result )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-03-31 03:40:19 +00:00
# ── Static file path ─────────────────────────────────────────────────────────
2026-04-12 07:11:41 +00:00
_INDEX_HTML_PATH = REPO_ROOT / " static " / " index.html "
2026-03-31 03:40:19 +00:00
# ── Thread synchronisation ───────────────────────────────────────────────────
2026-04-12 07:11:41 +00:00
LOCK = threading . Lock ( )
SESSIONS_MAX = 100
CHAT_LOCK = threading . Lock ( )
STREAMS : dict = { }
STREAMS_LOCK = threading . Lock ( )
2026-03-31 03:40:19 +00:00
CANCEL_FLAGS : dict = { }
v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)
Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. <code> becoming &lt;code&gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.
Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.
Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.
Fixes #240
* fix: restore custom model list discovery with config api key (#238)
get_available_models() now reads api_key from config.yaml before env vars:
1. model.api_key
2. providers.<active>.api_key / providers.custom.api_key
3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)
Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.
Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.
* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)
Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.
Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.
Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.
* security: redact credentials from API responses and fix credential file permissions (#243)
Adds response-layer credential redaction to three endpoints:
- GET /api/session — messages[], tool_calls[], and title
- GET /api/session/export — download also redacted
- SSE done event — session payload in stream
- GET /api/memory — MEMORY.md and USER.md content
Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.
Merged with #237 container detection changes in server.py.
* fix: cancel button now interrupts agent and cleans up UI state (#244)
Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.
Changes:
- api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
- api/streaming.py: stores agent in AGENT_INSTANCES after creation,
checks CANCEL_FLAGS immediately after store (race condition fix),
calls agent.interrupt() in cancel_stream(), cleans up in finally block
- static/boot.js: removes stale setStatus(cancelling) call
- static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel
Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.
New test file: tests/test_cancel_interrupt.py with 6 unit tests.
* docs: v0.46.0 release notes, bump version, update test counts
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 17:17:52 +00:00
AGENT_INSTANCES : dict = { } # stream_id -> AIAgent instance for interrupt propagation
2026-04-23 18:16:59 +00:00
STREAM_PARTIAL_TEXT : dict = { } # stream_id -> partial assistant text accumulated during streaming
2026-03-31 03:40:19 +00:00
SERVER_START_TIME = time . time ( )
# ── Thread-local env context ─────────────────────────────────────────────────
_thread_ctx = threading . local ( )
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
def _set_thread_env ( * * kwargs ) :
_thread_ctx . env = kwargs
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
def _clear_thread_env ( ) :
_thread_ctx . env = { }
2026-04-12 07:11:41 +00:00
2026-03-31 03:40:19 +00:00
# ── Per-session agent locks ───────────────────────────────────────────────────
SESSION_AGENT_LOCKS : dict = { }
2026-04-12 07:11:41 +00:00
SESSION_AGENT_LOCKS_LOCK = threading . Lock ( )
2026-03-31 03:40:19 +00:00
def _get_session_agent_lock ( session_id : str ) - > threading . Lock :
2026-04-23 21:25:43 +00:00
""" Return the per-session Lock used to serialize all Session mutations.
Lock lifecycle invariant :
- A Lock is created lazily on first access and lives in SESSION_AGENT_LOCKS
for the lifetime of the session .
- The entry is pruned in / api / session / delete ( under SESSION_AGENT_LOCKS_LOCK )
so deleted sessions don ' t leak a Lock forever.
- During context compression the agent may rotate session_id . The
streaming thread migrates the lock entry atomically under
SESSION_AGENT_LOCKS_LOCK : it aliases the new session_id to the * same *
Lock object and pops the old - id entry ( see streaming . py compression
block ) . This ensures that subsequent callers using the new ID still
acquire the same Lock , while the old - id entry is removed to prevent a
leak . The streaming thread already holds the Lock during this
migration , so the reference stays alive even after the dict entry is
removed .
- Lock contract : hold for the in - memory mutation + s . save ( ) only ; never
across network I / O ( LLM calls , HTTP requests ) .
"""
2026-03-31 03:40:19 +00:00
with SESSION_AGENT_LOCKS_LOCK :
if session_id not in SESSION_AGENT_LOCKS :
SESSION_AGENT_LOCKS [ session_id ] = threading . Lock ( )
return SESSION_AGENT_LOCKS [ session_id ]
2026-04-12 07:11:41 +00:00
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# ── Settings persistence ─────────────────────────────────────────────────────
_SETTINGS_DEFAULTS = {
2026-04-12 07:11:41 +00:00
" default_workspace " : str ( DEFAULT_WORKSPACE ) ,
" onboarding_completed " : False ,
" send_key " : " enter " , # 'enter' or 'ctrl+enter'
" show_token_usage " : False , # show input/output token badge below assistant messages
" show_cli_sessions " : False , # merge CLI sessions from state.db into the sidebar
" sync_to_insights " : False , # mirror WebUI token usage to state.db for /insights
" check_for_updates " : True , # check if webui/agent repos are behind upstream
2026-04-18 06:37:09 +00:00
" theme " : " dark " , # light | dark | system
" skin " : " default " , # accent color skin: default | ares | mono | slate | poseidon | sisyphus | charizard
2026-04-12 07:11:41 +00:00
" language " : " en " , # UI locale code; must match a key in static/i18n.js LOCALES
" bot_name " : os . getenv (
" HERMES_WEBUI_BOT_NAME " , " Hermes "
) , # display name for the assistant
" sound_enabled " : False , # play notification sound when assistant finishes
" notifications_enabled " : False , # browser notification when tab is in background
2026-04-21 22:26:52 +00:00
" show_thinking " : True , # show/hide thinking/reasoning blocks in chat view
2026-04-20 19:43:40 +00:00
" sidebar_density " : " compact " , # compact | detailed
2026-04-12 07:11:41 +00:00
" password_hash " : None , # PBKDF2-HMAC-SHA256 hash; None = auth disabled
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
}
2026-04-20 22:34:45 +00:00
_SETTINGS_LEGACY_DROP_KEYS = { " assistant_language " , " bubble_layout " , " default_model " }
2026-04-18 06:37:09 +00:00
_SETTINGS_THEME_VALUES = { " light " , " dark " , " system " }
_SETTINGS_SKIN_VALUES = {
" default " ,
" ares " ,
" mono " ,
" slate " ,
" poseidon " ,
" sisyphus " ,
" charizard " ,
}
_SETTINGS_LEGACY_THEME_MAP = {
# Legacy full themes now map onto the closest supported theme + accent skin pair.
" slate " : ( " dark " , " slate " ) ,
" solarized " : ( " dark " , " poseidon " ) ,
" monokai " : ( " dark " , " sisyphus " ) ,
" nord " : ( " dark " , " slate " ) ,
" oled " : ( " dark " , " default " ) ,
}
def _normalize_appearance ( theme , skin ) - > tuple [ str , str ] :
""" Normalize a (theme, skin) pair, migrating legacy theme names.
Legacy migration table ( from ` _SETTINGS_LEGACY_THEME_MAP ` ) :
slate → ( " dark " , " slate " )
solarized → ( " dark " , " poseidon " )
monokai → ( " dark " , " sisyphus " )
nord → ( " dark " , " slate " )
oled → ( " dark " , " default " )
Unknown / custom theme names fall back to ( " dark " , " default " ) . This is a
behavior change vs . the pre - PR - #627 state, where the `theme` field was
open - ended ( " no enum gate -- allows custom themes " ) . Users who set a
custom CSS theme via ` data - theme ` will need to re - apply via skin or
custom CSS — see CHANGELOG entry for details .
The same mapping is mirrored in ` static / boot . js ` ( ` _LEGACY_THEME_MAP ` )
so client and server normalize identically ; keep them in sync .
"""
raw_theme = theme . strip ( ) . lower ( ) if isinstance ( theme , str ) else " "
raw_skin = skin . strip ( ) . lower ( ) if isinstance ( skin , str ) else " "
legacy = _SETTINGS_LEGACY_THEME_MAP . get ( raw_theme )
if legacy :
next_theme , legacy_skin = legacy
elif raw_theme in _SETTINGS_THEME_VALUES :
next_theme , legacy_skin = raw_theme , " default "
else :
# Unknown themes used to exist; default to dark so upgrades stay visually stable.
next_theme , legacy_skin = " dark " , " default "
next_skin = (
raw_skin
if raw_skin in _SETTINGS_SKIN_VALUES
else legacy_skin
)
return next_theme , next_skin
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-04-12 07:11:41 +00:00
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
def load_settings ( ) - > dict :
""" Load settings from disk, merging with defaults for any missing keys. """
settings = dict ( _SETTINGS_DEFAULTS )
2026-04-18 06:37:09 +00:00
stored = None
2026-04-17 03:16:07 +00:00
try :
settings_exists = SETTINGS_FILE . exists ( )
except OSError :
# PermissionError or other OS-level error (e.g. UID mismatch in Docker)
# Treat as missing — start with defaults rather than crashing.
logger . debug ( " Cannot stat settings file %s (inaccessible?) " , SETTINGS_FILE )
settings_exists = False
if settings_exists :
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
try :
2026-04-12 07:11:41 +00:00
stored = json . loads ( SETTINGS_FILE . read_text ( encoding = " utf-8 " ) )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
if isinstance ( stored , dict ) :
2026-04-14 12:51:19 +00:00
settings . update (
{
k : v
for k , v in stored . items ( )
if k not in _SETTINGS_LEGACY_DROP_KEYS
}
)
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
except Exception :
security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)
- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists
* fix: handle ConnectionResetError gracefully and add debug logging
- Add QuietHTTPServer class to suppress noisy connection reset errors
caused by clients disconnecting abruptly (fixes log spam from
'ConnectionResetError: [Errno 54] Connection reset by peer')
- Replace silent 'pass' statements with logger.debug() calls across
api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
and api/onboarding.py for better observability during troubleshooting
- All tests pass (25 passed in test_regressions.py)
* chore: add debug logging to profiles and routes modules
- Replace silent 'pass' statements with logger.debug() calls in
api/profiles.py for better error visibility during profile switching
and module patching
- Add logger initialization to api/routes.py
* security: fix B110 bare except/pass issues (bandit security scan)
- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully
* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)
---------
Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 18:11:56 +00:00
logger . debug ( " Failed to load settings from %s " , SETTINGS_FILE )
2026-04-18 06:37:09 +00:00
settings [ " theme " ] , settings [ " skin " ] = _normalize_appearance (
stored . get ( " theme " ) if isinstance ( stored , dict ) else settings . get ( " theme " ) ,
stored . get ( " skin " ) if isinstance ( stored , dict ) else settings . get ( " skin " ) ,
)
2026-04-20 22:12:01 +00:00
settings [ " default_model " ] = get_effective_default_model ( )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
return settings
2026-04-12 07:11:41 +00:00
2026-04-20 22:12:01 +00:00
_SETTINGS_ALLOWED_KEYS = set ( _SETTINGS_DEFAULTS . keys ( ) ) - {
" password_hash " ,
" default_model " ,
}
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
_SETTINGS_ENUM_VALUES = {
2026-04-12 07:11:41 +00:00
" send_key " : { " enter " , " ctrl+enter " } ,
2026-04-20 19:43:40 +00:00
" sidebar_density " : { " compact " , " detailed " } ,
2026-04-12 07:11:41 +00:00
}
_SETTINGS_BOOL_KEYS = {
" onboarding_completed " ,
" show_token_usage " ,
" show_cli_sessions " ,
" sync_to_insights " ,
" check_for_updates " ,
" sound_enabled " ,
" notifications_enabled " ,
2026-04-21 22:26:52 +00:00
" show_thinking " ,
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
}
2026-04-08 14:55:03 +00:00
# Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr')
2026-04-12 07:11:41 +00:00
_SETTINGS_LANG_RE = __import__ ( " re " ) . compile ( r " ^[a-zA-Z] { 2,10}(-[a-zA-Z0-9] { 2,8})?$ " )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
def save_settings ( settings : dict ) - > dict :
""" Save settings to disk. Returns the merged settings. Ignores unknown keys. """
current = load_settings ( )
2026-04-18 06:37:09 +00:00
pending_theme = current . get ( " theme " )
pending_skin = current . get ( " skin " )
theme_was_explicit = False
skin_was_explicit = False
feat: Sprint 19 — password auth, security headers, login page
Auth system (off by default, zero friction for localhost):
- New api/auth.py module: password hashing (SHA-256 + STATE_DIR salt),
signed HMAC session cookies (24h TTL), auth middleware
- Enable via HERMES_WEBUI_PASSWORD env var or Settings panel
- Minimal dark-themed login page at /login (self-contained HTML)
- POST /api/auth/login, /api/auth/logout, GET /api/auth/status
- Settings panel: "Access Password" field + "Sign Out" button
- password_hash added to settings.json (null = auth disabled)
Security hardening:
- Security headers on all responses: X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin
- POST body size limit: 20MB cap in read_body() to prevent DoS
Closes #23. 9 new tests. Total: 304 passed, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:53:26 +00:00
# Handle _set_password: hash and store as password_hash
2026-04-12 07:11:41 +00:00
raw_pw = settings . pop ( " _set_password " , None )
feat: Sprint 19 — password auth, security headers, login page
Auth system (off by default, zero friction for localhost):
- New api/auth.py module: password hashing (SHA-256 + STATE_DIR salt),
signed HMAC session cookies (24h TTL), auth middleware
- Enable via HERMES_WEBUI_PASSWORD env var or Settings panel
- Minimal dark-themed login page at /login (self-contained HTML)
- POST /api/auth/login, /api/auth/logout, GET /api/auth/status
- Settings panel: "Access Password" field + "Sign Out" button
- password_hash added to settings.json (null = auth disabled)
Security hardening:
- Security headers on all responses: X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, Referrer-Policy: same-origin
- POST body size limit: 20MB cap in read_body() to prevent DoS
Closes #23. 9 new tests. Total: 304 passed, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:53:26 +00:00
if raw_pw and isinstance ( raw_pw , str ) and raw_pw . strip ( ) :
release: v0.39.0 — security hardening, 12 fixes (#171)
* Security: harden auth, CSRF, SSRF, XSS, and env race conditions
Twelve fixes from a full security audit:
CRITICAL
- Add CSRF Origin/Referer validation on all POST endpoints
(prevents cross-origin abuse of self-update, settings, file ops)
HIGH
- Unify password hashing: config.py now uses PBKDF2 (600k iters)
instead of single-iteration SHA-256
- Add per-IP rate limiting on login (5 attempts/60s, 429 on excess)
MEDIUM
- Validate session IDs as hex-only before filesystem operations
(prevents path traversal via crafted session ID)
- SSRF: resolve DNS before private-IP check in model fetching
(prevents DNS rebinding to internal services)
- Warn loudly when binding non-loopback without password set
- SSE env var mutations: wrap sync chat + streaming restore in _ENV_LOCK
- Force Content-Disposition:attachment for HTML/XHTML/SVG uploads
(prevents stored XSS via uploaded files)
LOW
- Extend HMAC session signature from 64 to 128 bits
- Add resolve()+relative_to() check on skills path construction
- Set Secure flag on session cookie when connection is HTTPS
- Sanitize exception messages to strip filesystem paths
No breaking changes. All fixes are backward-compatible.
* fix: use getattr for Secure cookie SSL detection
handler.request.getpeercert raises AttributeError on plain sockets
(non-SSL). Use getattr(..., None) to safely check for SSL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tests: add sprint 29 security hardening coverage (PR #171)
33 tests covering all 12 security fixes:
- CSRF origin/referer validation
- Login rate limiting (5 attempts/60s)
- Session ID hex validation (path traversal prevention)
- Error path sanitization (_sanitize_error)
- Secure cookie getattr safety
- HMAC signature length (64->128 bit)
- Skills path traversal prevention
- Content-Disposition for HTML/SVG/XHTML
- PBKDF2 password hashing verification
- Non-loopback startup warning
- SSRF DNS guard code presence
- _ENV_LOCK export from streaming module
* release: v0.39.0 — security hardening, 12 fixes (#171)
---------
Co-authored-by: betamod <matthew.sloly@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 05:26:03 +00:00
# Use PBKDF2 from auth module (600k iterations) -- never raw SHA-256
from api . auth import _hash_password
2026-04-12 07:11:41 +00:00
current [ " password_hash " ] = _hash_password ( raw_pw . strip ( ) )
2026-04-03 13:17:32 +00:00
# Handle _clear_password: explicitly disable auth
2026-04-12 07:11:41 +00:00
if settings . pop ( " _clear_password " , False ) :
current [ " password_hash " ] = None
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
for k , v in settings . items ( ) :
if k in _SETTINGS_ALLOWED_KEYS :
2026-04-18 06:37:09 +00:00
if k == " theme " :
if isinstance ( v , str ) and v . strip ( ) :
pending_theme = v
theme_was_explicit = True
continue
if k == " skin " :
if isinstance ( v , str ) and v . strip ( ) :
pending_skin = v
skin_was_explicit = True
continue
feat: Sprint 17 -- workspace breadcrumbs, slash commands, send key setting
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes #26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:31:31 +00:00
# Validate enum-constrained keys
if k in _SETTINGS_ENUM_VALUES and v not in _SETTINGS_ENUM_VALUES [ k ] :
continue
2026-04-08 14:55:03 +00:00
# Validate language codes (BCP-47-like: 'en', 'zh', 'fr', 'zh-CN')
2026-04-12 07:11:41 +00:00
if k == " language " and (
not isinstance ( v , str ) or not _SETTINGS_LANG_RE . match ( v )
) :
2026-04-08 14:55:03 +00:00
continue
2026-04-04 02:04:41 +00:00
# Coerce bool keys
if k in _SETTINGS_BOOL_KEYS :
v = bool ( v )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
current [ k ] = v
2026-04-18 06:37:09 +00:00
theme_value = pending_theme
skin_value = pending_skin
if theme_was_explicit and not skin_was_explicit :
raw_theme = pending_theme . strip ( ) . lower ( ) if isinstance ( pending_theme , str ) else " "
if raw_theme not in _SETTINGS_THEME_VALUES :
skin_value = None
current [ " theme " ] , current [ " skin " ] = _normalize_appearance ( theme_value , skin_value )
2026-04-13 21:28:24 +00:00
current [ " default_workspace " ] = str (
resolve_default_workspace ( current . get ( " default_workspace " ) )
)
2026-04-20 22:12:01 +00:00
persisted = { k : v for k , v in current . items ( ) if k != " default_model " }
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
SETTINGS_FILE . write_text (
2026-04-20 22:12:01 +00:00
json . dumps ( persisted , ensure_ascii = False , indent = 2 ) ,
2026-04-12 07:11:41 +00:00
encoding = " utf-8 " ,
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
)
# Update runtime defaults so new sessions use them immediately
2026-04-20 22:12:01 +00:00
global DEFAULT_WORKSPACE
2026-04-12 07:11:41 +00:00
if " default_workspace " in current :
2026-04-13 21:28:24 +00:00
DEFAULT_WORKSPACE = resolve_default_workspace ( current [ " default_workspace " ] )
2026-04-20 22:12:01 +00:00
current [ " default_model " ] = get_effective_default_model ( )
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
return current
2026-04-12 07:11:41 +00:00
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
# Apply saved settings on startup (override env-derived defaults)
2026-04-17 01:09:16 +00:00
# Exception: if HERMES_WEBUI_DEFAULT_WORKSPACE is explicitly set in the
# environment, it wins over whatever settings.json has stored. Persisted
# config must never shadow an explicit env-var override (Docker deployments
# rely on this — otherwise deleting settings.json is the only escape).
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
_startup_settings = load_settings ( )
2026-04-17 03:16:07 +00:00
try :
_settings_file_exists = SETTINGS_FILE . exists ( )
except OSError :
_settings_file_exists = False
if _settings_file_exists :
2026-04-17 01:09:16 +00:00
if not os . getenv ( " HERMES_WEBUI_DEFAULT_WORKSPACE " ) :
DEFAULT_WORKSPACE = resolve_default_workspace (
_startup_settings . get ( " default_workspace " )
)
2026-04-20 22:12:01 +00:00
_startup_settings . pop ( " default_model " , None ) # always drop stale value; model comes from config.yaml
2026-04-13 21:28:24 +00:00
if _startup_settings . get ( " default_workspace " ) != str ( DEFAULT_WORKSPACE ) :
_startup_settings [ " default_workspace " ] = str ( DEFAULT_WORKSPACE )
try :
SETTINGS_FILE . write_text (
json . dumps ( _startup_settings , ensure_ascii = False , indent = 2 ) ,
encoding = " utf-8 " ,
)
except Exception :
pass
Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)
Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export
Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name
Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)
Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
2026-03-31 03:40:19 +00:00
# ── SESSIONS in-memory cache (LRU OrderedDict) ───────────────────────────────
SESSIONS : collections . OrderedDict = collections . OrderedDict ( )
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
# ── Profile state initialisation ────────────────────────────────────────────
# Must run after all imports are resolved to correctly patch module-level caches
try :
from api . profiles import init_profile_state
2026-04-12 07:11:41 +00:00
feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI
profile system. Profiles are isolated HERMES_HOME instances with their own
config, skills, memory, cron, and API keys.
Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config
reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME
env save/restore in streaming, module-level cache patching for skills_tool
and cron/jobs.
Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with
CRUD UI, boot-time profile fetch, cascade refresh on switch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:50:21 +00:00
init_profile_state ( )
except ImportError :
pass # hermes_cli not available -- default profile only