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 -- Route handlers for GET and POST endpoints.
|
|
|
|
|
Extracted from server.py (Sprint 11) so server.py is a thin shell.
|
|
|
|
|
"""
|
2026-04-06 15:06:16 +00:00
|
|
|
import html as _html
|
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
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import queue
|
|
|
|
|
import sys
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from urllib.parse import parse_qs
|
|
|
|
|
|
|
|
|
|
from api.config import (
|
|
|
|
|
STATE_DIR, SESSION_DIR, DEFAULT_WORKSPACE, DEFAULT_MODEL,
|
|
|
|
|
SESSIONS, SESSIONS_MAX, LOCK, STREAMS, STREAMS_LOCK, CANCEL_FLAGS,
|
|
|
|
|
SERVER_START_TIME, CLI_TOOLSETS, _INDEX_HTML_PATH, get_available_models,
|
|
|
|
|
IMAGE_EXTS, MD_EXTS, MIME_MAP, MAX_FILE_BYTES, MAX_UPLOAD_BYTES,
|
|
|
|
|
CHAT_LOCK, load_settings, save_settings,
|
|
|
|
|
)
|
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
|
|
|
from api.helpers import require, bad, safe_resolve, j, t, read_body, _security_headers, _sanitize_error
|
|
|
|
|
|
|
|
|
|
# ── CSRF: validate Origin/Referer on POST ────────────────────────────────────
|
|
|
|
|
import re as _re
|
|
|
|
|
def _check_csrf(handler) -> bool:
|
|
|
|
|
"""Reject cross-origin POST requests. Returns True if OK."""
|
|
|
|
|
origin = handler.headers.get('Origin', '')
|
|
|
|
|
referer = handler.headers.get('Referer', '')
|
|
|
|
|
host = handler.headers.get('Host', '')
|
|
|
|
|
if not origin and not referer:
|
|
|
|
|
return True # non-browser clients (curl, agent) have no Origin
|
|
|
|
|
target = origin or referer
|
|
|
|
|
# Allow same-origin: Origin must match Host
|
|
|
|
|
if host and target:
|
|
|
|
|
# Extract host:port from origin/referer
|
|
|
|
|
m = _re.match(r'^https?://([^/]+)', target)
|
|
|
|
|
if m and m.group(1) == host:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
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
|
|
|
from api.models import (
|
|
|
|
|
Session, get_session, new_session, all_sessions, title_from,
|
|
|
|
|
_write_session_index, SESSION_INDEX_FILE,
|
2026-04-04 02:54:54 +00:00
|
|
|
load_projects, save_projects, import_cli_session,
|
|
|
|
|
get_cli_sessions, get_cli_session_messages,
|
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
|
|
|
)
|
|
|
|
|
from api.workspace import (
|
|
|
|
|
load_workspaces, save_workspaces, get_last_workspace, set_last_workspace,
|
|
|
|
|
list_dir, read_file_content, safe_resolve_ws,
|
|
|
|
|
)
|
|
|
|
|
from api.upload import handle_upload
|
|
|
|
|
from api.streaming import _sse, _run_agent_streaming, cancel_stream
|
|
|
|
|
|
|
|
|
|
# Approval system (optional -- graceful fallback if agent not available)
|
|
|
|
|
try:
|
|
|
|
|
from tools.approval import (
|
|
|
|
|
has_pending, pop_pending, submit_pending,
|
|
|
|
|
approve_session, approve_permanent, save_permanent_allowlist,
|
|
|
|
|
is_approved, _pending, _lock, _permanent_approved,
|
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
|
|
|
resolve_gateway_approval,
|
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 ImportError:
|
|
|
|
|
has_pending = lambda *a, **k: False
|
|
|
|
|
pop_pending = lambda *a, **k: None
|
|
|
|
|
submit_pending = lambda *a, **k: None
|
|
|
|
|
approve_session = lambda *a, **k: None
|
|
|
|
|
approve_permanent = lambda *a, **k: None
|
|
|
|
|
save_permanent_allowlist = lambda *a, **k: None
|
|
|
|
|
is_approved = lambda *a, **k: True
|
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
|
|
|
resolve_gateway_approval = lambda *a, **k: 0
|
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
|
|
|
_pending = {}
|
|
|
|
|
_lock = threading.Lock()
|
|
|
|
|
_permanent_approved = set()
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 14:55:03 +00:00
|
|
|
# ── Login page locale strings ─────────────────────────────────────────────────
|
|
|
|
|
# Add entries here to support more languages on the login page.
|
|
|
|
|
# The key must match the 'language' setting value (from static/i18n.js LOCALES).
|
|
|
|
|
_LOGIN_LOCALE = {
|
|
|
|
|
'en': {
|
|
|
|
|
'lang': 'en', 'title': 'Sign in',
|
|
|
|
|
'subtitle': 'Enter your password to continue',
|
|
|
|
|
'placeholder': 'Password', 'btn': 'Sign in',
|
|
|
|
|
'invalid_pw': 'Invalid password', 'conn_failed': 'Connection failed',
|
|
|
|
|
},
|
|
|
|
|
'zh': {
|
|
|
|
|
'lang': 'zh-CN', 'title': '\u767b\u5f55',
|
|
|
|
|
'subtitle': '\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528',
|
|
|
|
|
'placeholder': '\u5bc6\u7801', 'btn': '\u767b\u5f55',
|
|
|
|
|
'invalid_pw': '\u5bc6\u7801\u9519\u8bef', 'conn_failed': '\u8fde\u63a5\u5931\u8d25',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
# ── Login page (self-contained, no external deps) ────────────────────────────
|
|
|
|
|
_LOGIN_PAGE_HTML = '''<!doctype html>
|
2026-04-08 14:55:03 +00:00
|
|
|
<html lang="{{LANG}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
|
<title>{{BOT_NAME}} — {{LOGIN_TITLE}}</title>
|
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
|
|
|
<style>
|
|
|
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
|
|
|
body{background:#1a1a2e;color:#e8e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
|
|
|
|
|
height:100vh;display:flex;align-items:center;justify-content:center}
|
|
|
|
|
.card{background:#16213e;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:36px 32px;
|
|
|
|
|
width:320px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.3)}
|
|
|
|
|
.logo{width:48px;height:48px;border-radius:12px;background:linear-gradient(145deg,#e8a030,#e94560);
|
|
|
|
|
display:flex;align-items:center;justify-content:center;font-weight:800;font-size:20px;color:#fff;
|
|
|
|
|
margin:0 auto 12px;box-shadow:0 2px 12px rgba(233,69,96,.3)}
|
|
|
|
|
h1{font-size:18px;font-weight:600;margin-bottom:4px}
|
|
|
|
|
.sub{font-size:12px;color:#8888aa;margin-bottom:24px}
|
|
|
|
|
input{width:100%;padding:10px 14px;border-radius:10px;border:1px solid rgba(255,255,255,.1);
|
|
|
|
|
background:rgba(255,255,255,.04);color:#e8e8f0;font-size:14px;outline:none;margin-bottom:14px;
|
|
|
|
|
transition:border-color .15s}
|
|
|
|
|
input:focus{border-color:rgba(124,185,255,.5);box-shadow:0 0 0 3px rgba(124,185,255,.1)}
|
|
|
|
|
button{width:100%;padding:10px;border-radius:10px;border:none;background:rgba(124,185,255,.15);
|
|
|
|
|
border:1px solid rgba(124,185,255,.3);color:#7cb9ff;font-size:14px;font-weight:600;cursor:pointer;
|
|
|
|
|
transition:all .15s}
|
|
|
|
|
button:hover{background:rgba(124,185,255,.25)}
|
|
|
|
|
.err{color:#e94560;font-size:12px;margin-top:10px;display:none}
|
|
|
|
|
</style></head><body>
|
|
|
|
|
<div class="card">
|
2026-04-06 14:47:00 +00:00
|
|
|
<div class="logo">{{BOT_NAME_INITIAL}}</div>
|
|
|
|
|
<h1>{{BOT_NAME}}</h1>
|
2026-04-08 14:55:03 +00:00
|
|
|
<p class="sub">{{LOGIN_SUBTITLE}}</p>
|
2026-04-05 19:45:57 +00:00
|
|
|
<form onsubmit="doLogin(event);return false">
|
2026-04-08 14:55:03 +00:00
|
|
|
<input type="password" id="pw" placeholder="{{LOGIN_PLACEHOLDER}}" autofocus
|
2026-04-05 19:45:57 +00:00
|
|
|
onkeydown="if(event.key==='Enter'){doLogin(event);event.preventDefault();}">
|
2026-04-08 14:55:03 +00:00
|
|
|
<button type="submit">{{LOGIN_BTN}}</button>
|
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
|
|
|
</form>
|
|
|
|
|
<div class="err" id="err"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
async function doLogin(e){
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const pw=document.getElementById('pw').value;
|
|
|
|
|
const err=document.getElementById('err');
|
|
|
|
|
err.style.display='none';
|
|
|
|
|
try{
|
|
|
|
|
const res=await fetch('/api/auth/login',{method:'POST',
|
|
|
|
|
headers:{'Content-Type':'application/json'},
|
|
|
|
|
body:JSON.stringify({password:pw}),credentials:'include'});
|
|
|
|
|
const data=await res.json();
|
|
|
|
|
if(res.ok&&data.ok){window.location.href='/';}
|
2026-04-08 14:55:03 +00:00
|
|
|
else{err.textContent=data.error||'{{LOGIN_INVALID_PW}}';err.style.display='block';}
|
|
|
|
|
}catch(ex){err.textContent='{{LOGIN_CONN_FAILED}}';err.style.display='block';}
|
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
|
|
|
}
|
|
|
|
|
</script></body></html>'''
|
|
|
|
|
|
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
|
|
|
# ── GET routes ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-05 06:30:20 +00:00
|
|
|
def handle_get(handler, parsed) -> bool:
|
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
|
|
|
"""Handle all GET routes. Returns True if handled, False for 404."""
|
|
|
|
|
|
|
|
|
|
if parsed.path in ('/', '/index.html'):
|
|
|
|
|
return t(handler, _INDEX_HTML_PATH.read_text(encoding='utf-8'),
|
|
|
|
|
content_type='text/html; charset=utf-8')
|
|
|
|
|
|
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 parsed.path == '/login':
|
2026-04-08 14:55:03 +00:00
|
|
|
_settings = load_settings()
|
|
|
|
|
_bn = _html.escape(_settings.get('bot_name') or 'Hermes')
|
|
|
|
|
_lang = _settings.get('language', 'en')
|
|
|
|
|
_login_strings = _LOGIN_LOCALE.get(_lang, _LOGIN_LOCALE['en'])
|
|
|
|
|
_page = (
|
|
|
|
|
_LOGIN_PAGE_HTML
|
|
|
|
|
.replace('{{BOT_NAME}}', _bn)
|
|
|
|
|
.replace('{{BOT_NAME_INITIAL}}', _bn[0].upper())
|
|
|
|
|
.replace('{{LANG}}', _html.escape(_login_strings['lang']))
|
|
|
|
|
.replace('{{LOGIN_TITLE}}', _html.escape(_login_strings['title']))
|
|
|
|
|
.replace('{{LOGIN_SUBTITLE}}', _html.escape(_login_strings['subtitle']))
|
|
|
|
|
.replace('{{LOGIN_PLACEHOLDER}}', _html.escape(_login_strings['placeholder']))
|
|
|
|
|
.replace('{{LOGIN_BTN}}', _html.escape(_login_strings['btn']))
|
2026-04-09 02:07:00 +00:00
|
|
|
.replace('{{LOGIN_INVALID_PW}}', _login_strings['invalid_pw'].replace('\\','\\\\').replace("'","\\'"))
|
|
|
|
|
.replace('{{LOGIN_CONN_FAILED}}', _login_strings['conn_failed'].replace('\\','\\\\').replace("'","\\'"))
|
2026-04-08 14:55:03 +00:00
|
|
|
)
|
2026-04-06 05:10:59 +00:00
|
|
|
return t(handler, _page, content_type='text/html; charset=utf-8')
|
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 parsed.path == '/api/auth/status':
|
|
|
|
|
from api.auth import is_auth_enabled, parse_cookie, verify_session
|
|
|
|
|
logged_in = False
|
|
|
|
|
if is_auth_enabled():
|
|
|
|
|
cv = parse_cookie(handler)
|
|
|
|
|
logged_in = bool(cv and verify_session(cv))
|
|
|
|
|
return j(handler, {'auth_enabled': is_auth_enabled(), 'logged_in': logged_in})
|
|
|
|
|
|
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 parsed.path == '/favicon.ico':
|
|
|
|
|
handler.send_response(204); handler.end_headers(); return True
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/health':
|
|
|
|
|
with STREAMS_LOCK: n_streams = len(STREAMS)
|
|
|
|
|
return j(handler, {
|
|
|
|
|
'status': 'ok', 'sessions': len(SESSIONS),
|
|
|
|
|
'active_streams': n_streams,
|
|
|
|
|
'uptime_seconds': round(time.time() - SERVER_START_TIME, 1),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/models':
|
|
|
|
|
return j(handler, get_available_models())
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/settings':
|
2026-04-03 13:05:41 +00:00
|
|
|
settings = load_settings()
|
|
|
|
|
# Never expose the stored password hash to clients
|
|
|
|
|
settings.pop('password_hash', None)
|
|
|
|
|
return j(handler, settings)
|
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 parsed.path.startswith('/static/'):
|
|
|
|
|
return _serve_static(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session':
|
|
|
|
|
sid = parse_qs(parsed.query).get('session_id', [''])[0]
|
|
|
|
|
if not sid:
|
|
|
|
|
return j(handler, {'error': 'session_id is required'}, status=400)
|
2026-04-04 02:54:54 +00:00
|
|
|
try:
|
|
|
|
|
s = get_session(sid)
|
|
|
|
|
return j(handler, {'session': s.compact() | {
|
|
|
|
|
'messages': s.messages,
|
|
|
|
|
'tool_calls': getattr(s, 'tool_calls', []),
|
|
|
|
|
}})
|
|
|
|
|
except KeyError:
|
|
|
|
|
# Not a WebUI session -- try CLI store
|
|
|
|
|
msgs = get_cli_session_messages(sid)
|
|
|
|
|
if msgs:
|
|
|
|
|
cli_meta = None
|
|
|
|
|
for cs in get_cli_sessions():
|
|
|
|
|
if cs['session_id'] == sid:
|
|
|
|
|
cli_meta = cs
|
|
|
|
|
break
|
|
|
|
|
sess = {
|
|
|
|
|
'session_id': sid,
|
|
|
|
|
'title': (cli_meta or {}).get('title', 'CLI Session'),
|
|
|
|
|
'workspace': (cli_meta or {}).get('workspace', ''),
|
|
|
|
|
'model': (cli_meta or {}).get('model', 'unknown'),
|
|
|
|
|
'message_count': len(msgs),
|
|
|
|
|
'created_at': (cli_meta or {}).get('created_at', 0),
|
|
|
|
|
'updated_at': (cli_meta or {}).get('updated_at', 0),
|
|
|
|
|
'pinned': False,
|
|
|
|
|
'archived': False,
|
|
|
|
|
'project_id': None,
|
|
|
|
|
'profile': (cli_meta or {}).get('profile'),
|
|
|
|
|
'is_cli_session': True,
|
|
|
|
|
'messages': msgs,
|
|
|
|
|
'tool_calls': [],
|
|
|
|
|
}
|
|
|
|
|
return j(handler, {'session': sess})
|
|
|
|
|
return bad(handler, 'Session not found', 404)
|
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 parsed.path == '/api/sessions':
|
2026-04-04 02:54:54 +00:00
|
|
|
webui_sessions = all_sessions()
|
2026-04-04 04:06:23 +00:00
|
|
|
settings = load_settings()
|
|
|
|
|
if settings.get('show_cli_sessions'):
|
|
|
|
|
cli = get_cli_sessions()
|
|
|
|
|
webui_ids = {s['session_id'] for s in webui_sessions}
|
|
|
|
|
deduped_cli = [s for s in cli if s['session_id'] not in webui_ids]
|
|
|
|
|
else:
|
|
|
|
|
deduped_cli = []
|
2026-04-04 02:54:54 +00:00
|
|
|
merged = webui_sessions + deduped_cli
|
|
|
|
|
merged.sort(key=lambda s: s.get('updated_at', 0) or 0, reverse=True)
|
|
|
|
|
return j(handler, {'sessions': merged, 'cli_count': len(deduped_cli)})
|
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-02 06:55:21 +00:00
|
|
|
if parsed.path == '/api/projects':
|
|
|
|
|
return j(handler, {'projects': load_projects()})
|
|
|
|
|
|
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 parsed.path == '/api/session/export':
|
|
|
|
|
return _handle_session_export(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/workspaces':
|
|
|
|
|
return j(handler, {'workspaces': load_workspaces(), 'last': get_last_workspace()})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/sessions/search':
|
|
|
|
|
return _handle_sessions_search(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/list':
|
|
|
|
|
return _handle_list_dir(handler, parsed)
|
|
|
|
|
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
if parsed.path == '/api/personalities':
|
2026-04-06 21:10:30 +00:00
|
|
|
# Read personalities from config.yaml agent.personalities section
|
|
|
|
|
# (matches hermes-agent CLI behavior, not filesystem SOUL.md approach)
|
|
|
|
|
from api.config import reload_config as _reload_cfg
|
|
|
|
|
_reload_cfg() # pick up config.yaml changes without server restart
|
|
|
|
|
from api.config import get_config as _get_cfg
|
|
|
|
|
_cfg = _get_cfg()
|
|
|
|
|
agent_cfg = _cfg.get('agent', {})
|
|
|
|
|
raw_personalities = agent_cfg.get('personalities', {})
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
personalities = []
|
2026-04-06 21:10:30 +00:00
|
|
|
if isinstance(raw_personalities, dict):
|
|
|
|
|
for name, value in raw_personalities.items():
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
desc = ''
|
2026-04-06 21:10:30 +00:00
|
|
|
if isinstance(value, dict):
|
|
|
|
|
desc = value.get('description', '')
|
|
|
|
|
elif isinstance(value, str):
|
|
|
|
|
desc = value[:80] + ('...' if len(value) > 80 else '')
|
|
|
|
|
personalities.append({'name': name, 'description': desc})
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
return j(handler, {'personalities': personalities})
|
|
|
|
|
|
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
|
|
|
if parsed.path == '/api/git-info':
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
sid = qs.get('session_id', [''])[0]
|
|
|
|
|
if not sid:
|
|
|
|
|
return bad(handler, 'session_id required')
|
2026-04-04 21:29:24 +00:00
|
|
|
try:
|
|
|
|
|
s = get_session(sid)
|
|
|
|
|
except KeyError:
|
feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.
Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
untracked, ahead, behind
Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes
Inspired by PR #75 (@MartinNielsenDev).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:08:25 +00:00
|
|
|
return bad(handler, 'Session not found', 404)
|
|
|
|
|
from api.workspace import git_info_for_workspace
|
|
|
|
|
info = git_info_for_workspace(Path(s.workspace))
|
|
|
|
|
return j(handler, {'git': info})
|
|
|
|
|
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
if parsed.path == '/api/updates/check':
|
|
|
|
|
settings = load_settings()
|
|
|
|
|
if not settings.get('check_for_updates', True):
|
|
|
|
|
return j(handler, {'disabled': True})
|
2026-04-05 16:20:12 +00:00
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
force = qs.get('force', ['0'])[0] == '1'
|
|
|
|
|
# ?simulate=1 returns fake behind counts for UI testing (localhost only)
|
|
|
|
|
if qs.get('simulate', ['0'])[0] == '1' and handler.client_address[0] == '127.0.0.1':
|
|
|
|
|
return j(handler, {
|
|
|
|
|
'webui': {'name': 'webui', 'behind': 3, 'current_sha': 'abc1234', 'latest_sha': 'def5678', 'branch': 'master'},
|
|
|
|
|
'agent': {'name': 'agent', 'behind': 1, 'current_sha': 'aaa0001', 'latest_sha': 'bbb0002', 'branch': 'master'},
|
|
|
|
|
'checked_at': 0,
|
|
|
|
|
})
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
from api.updates import check_for_updates
|
|
|
|
|
return j(handler, check_for_updates(force=force))
|
|
|
|
|
|
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 parsed.path == '/api/chat/stream/status':
|
|
|
|
|
stream_id = parse_qs(parsed.query).get('stream_id', [''])[0]
|
|
|
|
|
return j(handler, {'active': stream_id in STREAMS, 'stream_id': stream_id})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/chat/cancel':
|
|
|
|
|
stream_id = parse_qs(parsed.query).get('stream_id', [''])[0]
|
|
|
|
|
if not stream_id:
|
|
|
|
|
return bad(handler, 'stream_id required')
|
|
|
|
|
cancelled = cancel_stream(stream_id)
|
|
|
|
|
return j(handler, {'ok': True, 'cancelled': cancelled, 'stream_id': stream_id})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/chat/stream':
|
|
|
|
|
return _handle_sse_stream(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file/raw':
|
|
|
|
|
return _handle_file_raw(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file':
|
|
|
|
|
return _handle_file_read(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/approval/pending':
|
|
|
|
|
return _handle_approval_pending(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/approval/inject_test':
|
2026-04-02 06:46:40 +00:00
|
|
|
# Loopback-only: used by automated tests; blocked from any remote client
|
|
|
|
|
if handler.client_address[0] != '127.0.0.1':
|
|
|
|
|
return j(handler, {'error': 'not found'}, status=404)
|
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 _handle_approval_inject(handler, parsed)
|
|
|
|
|
|
|
|
|
|
# ── Cron API (GET) ──
|
|
|
|
|
if parsed.path == '/api/crons':
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import list_jobs
|
|
|
|
|
return j(handler, {'jobs': list_jobs(include_disabled=True)})
|
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 parsed.path == '/api/crons/output':
|
|
|
|
|
return _handle_cron_output(handler, parsed)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/recent':
|
|
|
|
|
return _handle_cron_recent(handler, parsed)
|
|
|
|
|
|
|
|
|
|
# ── Skills API (GET) ──
|
|
|
|
|
if parsed.path == '/api/skills':
|
|
|
|
|
from tools.skills_tool import skills_list as _skills_list
|
|
|
|
|
raw = _skills_list()
|
|
|
|
|
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
|
|
|
return j(handler, {'skills': data.get('skills', [])})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/skills/content':
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
from tools.skills_tool import skill_view as _skill_view, SKILLS_DIR
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
name = qs.get('name', [''])[0]
|
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 not name: return j(handler, {'error': 'name required'}, status=400)
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
file_path = qs.get('file', [''])[0]
|
|
|
|
|
if file_path:
|
|
|
|
|
# Serve a linked file from the skill directory
|
2026-04-04 01:54:53 +00:00
|
|
|
import re as _re
|
|
|
|
|
if _re.search(r'[*?\[\]]', name):
|
|
|
|
|
return bad(handler, 'Invalid skill name', 400)
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
skill_dir = None
|
|
|
|
|
for p in SKILLS_DIR.rglob(name):
|
|
|
|
|
if p.is_dir(): skill_dir = p; break
|
|
|
|
|
if not skill_dir: return bad(handler, 'Skill not found', 404)
|
|
|
|
|
target = (skill_dir / file_path).resolve()
|
|
|
|
|
try: target.relative_to(skill_dir.resolve())
|
|
|
|
|
except ValueError: return bad(handler, 'Invalid file path', 400)
|
|
|
|
|
if not target.exists() or not target.is_file():
|
|
|
|
|
return bad(handler, 'File not found', 404)
|
|
|
|
|
return j(handler, {'content': target.read_text(encoding='utf-8'), 'path': file_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
|
|
|
raw = _skill_view(name)
|
|
|
|
|
data = json.loads(raw) if isinstance(raw, str) else raw
|
feat: Sprint 23 — agentic transparency + polish
Track A: Token/cost display
- Read agent usage attrs (session_prompt_tokens, session_completion_tokens,
session_estimated_cost_usd) after run_conversation in streaming.py
- Add input_tokens, output_tokens, estimated_cost fields to Session model
- Include usage in done SSE event payload
- Store usage on S.lastUsage in messages.js done handler
- Render usage badge below last assistant message (input/output/cost)
Track B: Subagent delegation cards
- Add subagent_progress to toolIcon map with shuffle emoji
- Special-case subagent_progress in buildToolCard: "Subagent" label,
strip double emoji from preview, add tool-card-subagent CSS class
- Indented border-left styling for subagent cards
- Clean delegate_task display name
Track C: Skill picker in cron create form
- Add skill search input + tag chips to cron create form HTML
- Skill picker JS in panels.js: search/filter, click-to-add tags,
remove tag chips, pre-fetch skill list on form open
- submitCronCreate sends skills array in POST body
- Skill picker dropdown + tag CSS
Track D: Skill linked files viewer
- Add file query param to /api/skills/content endpoint
- Serve linked files from skill directory with path traversal protection
- Ensure linked_files key always present in skill content response
- Render linked files section below SKILL.md content in preview panel
- openSkillFile function for viewing individual linked files
Track E: Bug fixes and code quality
- Expand Session.__init__ and compact() to readable multi-line format
- Remove inline import json as _j2 inside loop in streaming.py
- Fix tool_calls: capture args from assistant messages, skip unresolved names
- Store args snapshot in persisted tool_calls for reload display
6 new tests. Total: 421 (409 passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:33:49 +00:00
|
|
|
if 'linked_files' not in data: data['linked_files'] = {}
|
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 j(handler, data)
|
|
|
|
|
|
|
|
|
|
# ── Memory API (GET) ──
|
|
|
|
|
if parsed.path == '/api/memory':
|
|
|
|
|
return _handle_memory_read(handler)
|
|
|
|
|
|
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 API (GET) ──
|
|
|
|
|
if parsed.path == '/api/profiles':
|
|
|
|
|
from api.profiles import list_profiles_api, get_active_profile_name
|
|
|
|
|
return j(handler, {'profiles': list_profiles_api(), 'active': get_active_profile_name()})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/profile/active':
|
|
|
|
|
from api.profiles import get_active_profile_name, get_active_hermes_home
|
|
|
|
|
return j(handler, {'name': get_active_profile_name(), 'path': str(get_active_hermes_home())})
|
|
|
|
|
|
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 False # 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── POST routes ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-05 06:30:20 +00:00
|
|
|
def handle_post(handler, parsed) -> bool:
|
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
|
|
|
"""Handle all POST routes. Returns True if handled, False for 404."""
|
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
|
|
|
# CSRF: reject cross-origin browser requests
|
|
|
|
|
if not _check_csrf(handler):
|
|
|
|
|
return j(handler, {'error': 'Cross-origin request rejected'}, status=403)
|
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 parsed.path == '/api/upload':
|
|
|
|
|
return handle_upload(handler)
|
|
|
|
|
|
|
|
|
|
body = read_body(handler)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session/new':
|
|
|
|
|
s = new_session(workspace=body.get('workspace'), model=body.get('model'))
|
|
|
|
|
return j(handler, {'session': s.compact() | {'messages': s.messages}})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/sessions/cleanup':
|
|
|
|
|
return _handle_sessions_cleanup(handler, body, zero_only=False)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/sessions/cleanup_zero_message':
|
|
|
|
|
return _handle_sessions_cleanup(handler, body, zero_only=True)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session/rename':
|
|
|
|
|
try: require(body, 'session_id', 'title')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
s.title = str(body['title']).strip()[:80] or 'Untitled'
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'session': s.compact()})
|
|
|
|
|
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
if parsed.path == '/api/personality/set':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
if 'name' not in body:
|
|
|
|
|
return bad(handler, 'Missing required field: name')
|
|
|
|
|
sid = body['session_id']
|
|
|
|
|
name = body['name'].strip()
|
|
|
|
|
try:
|
|
|
|
|
s = get_session(sid)
|
|
|
|
|
except KeyError:
|
|
|
|
|
return bad(handler, 'Session not found', 404)
|
2026-04-06 21:10:30 +00:00
|
|
|
# Resolve personality from config.yaml agent.personalities section
|
|
|
|
|
# (matches hermes-agent CLI behavior)
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
prompt = ''
|
|
|
|
|
if name:
|
2026-04-06 21:10:30 +00:00
|
|
|
from api.config import reload_config as _reload_cfg2
|
|
|
|
|
_reload_cfg2() # pick up config changes without restart
|
|
|
|
|
from api.config import get_config as _get_cfg2
|
|
|
|
|
_cfg2 = _get_cfg2()
|
|
|
|
|
agent_cfg = _cfg2.get('agent', {})
|
|
|
|
|
raw_personalities = agent_cfg.get('personalities', {})
|
|
|
|
|
if not isinstance(raw_personalities, dict) or name not in raw_personalities:
|
|
|
|
|
return bad(handler, f'Personality "{name}" not found in config.yaml', 404)
|
|
|
|
|
value = raw_personalities[name]
|
|
|
|
|
# Resolve prompt using the same logic as hermes-agent cli.py
|
|
|
|
|
if isinstance(value, dict):
|
|
|
|
|
parts = [value.get('system_prompt', '') or value.get('prompt', '')]
|
|
|
|
|
if value.get('tone'):
|
|
|
|
|
parts.append(f'Tone: {value["tone"]}')
|
|
|
|
|
if value.get('style'):
|
|
|
|
|
parts.append(f'Style: {value["style"]}')
|
|
|
|
|
prompt = '\n'.join(p for p in parts if p)
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
else:
|
2026-04-06 21:10:30 +00:00
|
|
|
prompt = str(value)
|
feat: /personality slash command with backend integration (#143)
* feat: /personality slash command with backend integration
Add /personality command to switch the agent's system prompt personality.
Hermes CLI supports personalities stored at ~/.hermes/personalities/<name>/SOUL.md.
Backend:
- GET /api/personalities: lists available personalities from the active
profile's personalities directory (reads first line of SOUL.md for desc)
- POST /api/personality/set: sets active personality on the session, reads
and validates the SOUL.md file exists, returns the prompt text
- streaming.py: injects personality prompt (SOUL.md content) as prefix to
the system_message when run_conversation is called
Frontend (commands.js):
- /personality with no args: lists available personalities as a local message
- /personality <name>: sets the personality with a toast confirmation
- /personality none|default|clear: removes the active personality
Session model: new 'personality' field (backward-compatible, defaults to None)
Closes #139
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: path traversal in personality name + case sensitivity
Security: personality name is now validated with regex ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$
in both routes.py (POST /api/personality/set) and streaming.py (system
prompt injection). Defense-in-depth: resolve().relative_to() check ensures
the path stays inside the personalities directory even if regex is bypassed.
Also: removed toLowerCase() from frontend command handler so personality
names are case-preserved (filesystem may be case-sensitive).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: /personality command — hardened, compact() fix, tests
Fixes on top of original PR:
- compact() was missing 'personality' field — UI couldn't know active
personality after page load. Added to Session.compact().
- GET /api/personalities: add symlink guard (is_symlink() skip) and
resolve() check — prevents reading SOUL.md from symlink targets
outside personalities dir.
- POST /api/personality/set: require() only checks session_id (not name)
so clearing with name='' works correctly instead of 400.
- POST /api/personality/set: add MAX_FILE_BYTES size cap on SOUL.md to
prevent unbounded context window consumption.
- POST /api/personality/set: return personality:null (not '') when cleared.
- streaming.py: same MAX_FILE_BYTES guard before prepending to system msg.
Added tests/test_sprint28.py: 11 tests for API round-trip, listing,
symlink guard, path traversal rejection, clear, size cap, persistence.
Tests pass in isolation; full-suite run has a test-isolation interaction
with shared server state across sprint tests (tracked as follow-up).
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:16:37 +00:00
|
|
|
s.personality = name if name else None
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'personality': s.personality, 'prompt': prompt})
|
|
|
|
|
|
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 parsed.path == '/api/session/update':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
new_ws = str(Path(body.get('workspace', s.workspace)).expanduser().resolve())
|
|
|
|
|
s.workspace = new_ws; s.model = body.get('model', s.model); s.save()
|
|
|
|
|
set_last_workspace(new_ws)
|
|
|
|
|
return j(handler, {'session': s.compact() | {'messages': s.messages}})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session/delete':
|
|
|
|
|
sid = body.get('session_id', '')
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
2026-04-05 00:33:55 +00:00
|
|
|
# Delete from WebUI session store
|
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
|
|
|
with LOCK: SESSIONS.pop(sid, None)
|
|
|
|
|
p = SESSION_DIR / f'{sid}.json'
|
|
|
|
|
try: p.unlink(missing_ok=True)
|
|
|
|
|
except Exception: pass
|
|
|
|
|
try: SESSION_INDEX_FILE.unlink(missing_ok=True)
|
|
|
|
|
except Exception: pass
|
2026-04-05 00:33:55 +00:00
|
|
|
# Also delete from CLI state.db (for CLI sessions shown in sidebar)
|
|
|
|
|
try:
|
|
|
|
|
from api.models import delete_cli_session
|
|
|
|
|
delete_cli_session(sid)
|
|
|
|
|
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
|
|
|
return j(handler, {'ok': True})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session/clear':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
s.messages = []; s.tool_calls = []; s.title = 'Untitled'; s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact()})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/session/truncate':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
if body.get('keep_count') is None:
|
|
|
|
|
return bad(handler, 'Missing required field(s): keep_count')
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
keep = int(body['keep_count'])
|
|
|
|
|
s.messages = s.messages[:keep]; s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact() | {'messages': s.messages}})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/chat/start':
|
|
|
|
|
return _handle_chat_start(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/chat':
|
|
|
|
|
return _handle_chat_sync(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── Cron API (POST) ──
|
|
|
|
|
if parsed.path == '/api/crons/create':
|
|
|
|
|
return _handle_cron_create(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/update':
|
|
|
|
|
return _handle_cron_update(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/delete':
|
|
|
|
|
return _handle_cron_delete(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/run':
|
|
|
|
|
return _handle_cron_run(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/pause':
|
|
|
|
|
return _handle_cron_pause(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/crons/resume':
|
|
|
|
|
return _handle_cron_resume(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── File ops (POST) ──
|
|
|
|
|
if parsed.path == '/api/file/delete':
|
|
|
|
|
return _handle_file_delete(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file/save':
|
|
|
|
|
return _handle_file_save(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file/create':
|
|
|
|
|
return _handle_file_create(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file/rename':
|
|
|
|
|
return _handle_file_rename(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/file/create-dir':
|
|
|
|
|
return _handle_create_dir(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── Workspace management (POST) ──
|
|
|
|
|
if parsed.path == '/api/workspaces/add':
|
|
|
|
|
return _handle_workspace_add(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/workspaces/remove':
|
|
|
|
|
return _handle_workspace_remove(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/workspaces/rename':
|
|
|
|
|
return _handle_workspace_rename(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── Approval (POST) ──
|
|
|
|
|
if parsed.path == '/api/approval/respond':
|
|
|
|
|
return _handle_approval_respond(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── Skills (POST) ──
|
|
|
|
|
if parsed.path == '/api/skills/save':
|
|
|
|
|
return _handle_skill_save(handler, body)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/skills/delete':
|
|
|
|
|
return _handle_skill_delete(handler, body)
|
|
|
|
|
|
|
|
|
|
# ── Memory (POST) ──
|
|
|
|
|
if parsed.path == '/api/memory/write':
|
|
|
|
|
return _handle_memory_write(handler, body)
|
|
|
|
|
|
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 API (POST) ──
|
|
|
|
|
if parsed.path == '/api/profile/switch':
|
|
|
|
|
name = body.get('name', '').strip()
|
|
|
|
|
if not name: return bad(handler, 'name is required')
|
|
|
|
|
try:
|
|
|
|
|
from api.profiles import switch_profile
|
|
|
|
|
result = switch_profile(name)
|
|
|
|
|
return j(handler, result)
|
|
|
|
|
except (ValueError, FileNotFoundError) as e:
|
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
|
|
|
return bad(handler, _sanitize_error(e), 404)
|
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 RuntimeError as e:
|
|
|
|
|
return bad(handler, str(e), 409)
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/profile/create':
|
|
|
|
|
name = body.get('name', '').strip()
|
|
|
|
|
if not name: return bad(handler, 'name is required')
|
2026-04-03 18:06:18 +00:00
|
|
|
import re as _re
|
|
|
|
|
if not _re.match(r'^[a-z0-9][a-z0-9_-]{0,63}$', name):
|
|
|
|
|
return bad(handler, 'Invalid profile name: lowercase letters, numbers, hyphens, underscores only')
|
|
|
|
|
clone_from = body.get('clone_from')
|
|
|
|
|
if clone_from is not None:
|
|
|
|
|
clone_from = str(clone_from).strip()
|
|
|
|
|
if not _re.match(r'^[a-z0-9][a-z0-9_-]{0,63}$', clone_from):
|
|
|
|
|
return bad(handler, 'Invalid clone_from name')
|
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
|
|
|
try:
|
|
|
|
|
from api.profiles import create_profile_api
|
|
|
|
|
result = create_profile_api(
|
|
|
|
|
name,
|
2026-04-03 18:06:18 +00:00
|
|
|
clone_from=clone_from,
|
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
|
|
|
clone_config=bool(body.get('clone_config', False)),
|
|
|
|
|
)
|
|
|
|
|
return j(handler, {'ok': True, 'profile': result})
|
|
|
|
|
except (ValueError, FileExistsError, RuntimeError) as e:
|
|
|
|
|
return bad(handler, str(e))
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/profile/delete':
|
|
|
|
|
name = body.get('name', '').strip()
|
|
|
|
|
if not name: return bad(handler, 'name is required')
|
|
|
|
|
try:
|
|
|
|
|
from api.profiles import delete_profile_api
|
|
|
|
|
result = delete_profile_api(name)
|
|
|
|
|
return j(handler, result)
|
|
|
|
|
except (ValueError, FileNotFoundError) as e:
|
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
|
|
|
return bad(handler, _sanitize_error(e))
|
2026-04-03 18:06:18 +00:00
|
|
|
except RuntimeError as e:
|
|
|
|
|
return bad(handler, str(e), 409)
|
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
|
|
|
|
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 (POST) ──
|
|
|
|
|
if parsed.path == '/api/settings':
|
2026-04-06 15:06:16 +00:00
|
|
|
if 'bot_name' in body:
|
|
|
|
|
body['bot_name'] = (str(body['bot_name']) or '').strip() or 'Hermes'
|
2026-04-03 13:17:32 +00:00
|
|
|
saved = save_settings(body)
|
|
|
|
|
saved.pop('password_hash', None) # never expose hash to client
|
|
|
|
|
return j(handler, saved)
|
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
|
|
|
|
|
|
|
|
# ── Session pin (POST) ──
|
|
|
|
|
if parsed.path == '/api/session/pin':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
s.pinned = bool(body.get('pinned', True))
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact()})
|
|
|
|
|
|
|
|
|
|
# ── Session archive (POST) ──
|
|
|
|
|
if parsed.path == '/api/session/archive':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
s.archived = bool(body.get('archived', True))
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact()})
|
|
|
|
|
|
2026-04-02 06:55:21 +00:00
|
|
|
# ── Session move to project (POST) ──
|
|
|
|
|
if parsed.path == '/api/session/move':
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
s.project_id = body.get('project_id') or None
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact()})
|
|
|
|
|
|
|
|
|
|
# ── Project CRUD (POST) ──
|
|
|
|
|
if parsed.path == '/api/projects/create':
|
|
|
|
|
try: require(body, 'name')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
2026-04-02 08:01:58 +00:00
|
|
|
import re as _re
|
|
|
|
|
name = body['name'].strip()[:128]
|
|
|
|
|
if not name: return bad(handler, 'name required')
|
|
|
|
|
color = body.get('color')
|
|
|
|
|
if color and not _re.match(r'^#[0-9a-fA-F]{3,8}$', color):
|
|
|
|
|
return bad(handler, 'Invalid color format')
|
2026-04-02 06:55:21 +00:00
|
|
|
projects = load_projects()
|
2026-04-02 08:01:58 +00:00
|
|
|
proj = {'project_id': uuid.uuid4().hex[:12], 'name': name, 'color': color, 'created_at': time.time()}
|
2026-04-02 06:55:21 +00:00
|
|
|
projects.append(proj)
|
|
|
|
|
save_projects(projects)
|
|
|
|
|
return j(handler, {'ok': True, 'project': proj})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/projects/rename':
|
|
|
|
|
try: require(body, 'project_id', 'name')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
2026-04-02 08:01:58 +00:00
|
|
|
import re as _re
|
2026-04-02 06:55:21 +00:00
|
|
|
projects = load_projects()
|
|
|
|
|
proj = next((p for p in projects if p['project_id'] == body['project_id']), None)
|
|
|
|
|
if not proj: return bad(handler, 'Project not found', 404)
|
2026-04-02 08:01:58 +00:00
|
|
|
proj['name'] = body['name'].strip()[:128]
|
|
|
|
|
if 'color' in body:
|
|
|
|
|
color = body['color']
|
|
|
|
|
if color and not _re.match(r'^#[0-9a-fA-F]{3,8}$', color):
|
|
|
|
|
return bad(handler, 'Invalid color format')
|
|
|
|
|
proj['color'] = color
|
2026-04-02 06:55:21 +00:00
|
|
|
save_projects(projects)
|
|
|
|
|
return j(handler, {'ok': True, 'project': proj})
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/projects/delete':
|
|
|
|
|
try: require(body, 'project_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
projects = load_projects()
|
|
|
|
|
proj = next((p for p in projects if p['project_id'] == body['project_id']), None)
|
|
|
|
|
if not proj: return bad(handler, 'Project not found', 404)
|
|
|
|
|
projects = [p for p in projects if p['project_id'] != body['project_id']]
|
|
|
|
|
save_projects(projects)
|
|
|
|
|
# Unassign all sessions that belonged to this project
|
|
|
|
|
if SESSION_INDEX_FILE.exists():
|
|
|
|
|
try:
|
|
|
|
|
index = json.loads(SESSION_INDEX_FILE.read_text(encoding='utf-8'))
|
|
|
|
|
for entry in index:
|
|
|
|
|
if entry.get('project_id') == body['project_id']:
|
|
|
|
|
try:
|
|
|
|
|
s = get_session(entry['session_id'])
|
|
|
|
|
s.project_id = None
|
|
|
|
|
s.save()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return j(handler, {'ok': True})
|
|
|
|
|
|
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
|
|
|
# ── Session import from JSON (POST) ──
|
|
|
|
|
if parsed.path == '/api/session/import':
|
|
|
|
|
return _handle_session_import(handler, body)
|
|
|
|
|
|
feat: self-update checker with one-click update for WebUI + Agent
Shows a blue banner when the webui or hermes-agent git repos are behind
their upstream branches. One-click 'Update Now' button does stash, pull
--ff-only, stash pop, then reloads the page.
Backend (api/updates.py):
- _check_repo(): git fetch + rev-list count with 15s timeout
- check_for_updates(): 30-min server-side cache, thread-safe, skips
Docker (no .git dir)
- apply_update(): stash (if dirty), pull --ff-only, pop, invalidate cache
Routes:
- GET /api/updates/check -- returns cached {webui, agent} with behind count
- POST /api/updates/apply -- {target: 'webui'|'agent'}
Frontend:
- Blue banner (matches reconnect-banner pattern) with 'Later' / 'Update Now'
- Non-blocking boot check via fire-and-forget .then(), once per tab session
- sessionStorage guards prevent re-checking and re-showing after dismiss
Settings:
- 'Check for updates' checkbox (default: on) -- when off, no git operations
- Removed 'Default Workspace' dropdown to keep settings panel compact
Performance:
- Server cache: git fetch at most 2x/hour regardless of client count
- sessionStorage: one check per browser tab session
- _check_in_progress flag prevents concurrent fetch storms
- Fire-and-forget: does NOT block the boot sequence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:11:44 +00:00
|
|
|
# ── Self-update (POST) ──
|
|
|
|
|
if parsed.path == '/api/updates/apply':
|
|
|
|
|
target = body.get('target', '')
|
|
|
|
|
if target not in ('webui', 'agent'):
|
|
|
|
|
return bad(handler, 'target must be "webui" or "agent"')
|
|
|
|
|
from api.updates import apply_update
|
|
|
|
|
return j(handler, apply_update(target))
|
|
|
|
|
|
2026-04-04 02:54:54 +00:00
|
|
|
# ── CLI session import (POST) ──
|
|
|
|
|
if parsed.path == '/api/session/import_cli':
|
|
|
|
|
return _handle_session_import_cli(handler, body)
|
|
|
|
|
|
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
|
|
|
# ── Auth endpoints (POST) ──
|
|
|
|
|
if parsed.path == '/api/auth/login':
|
|
|
|
|
from api.auth import verify_password, create_session, set_auth_cookie, is_auth_enabled
|
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
|
|
|
from api.auth import _check_login_rate, _record_login_attempt
|
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 not is_auth_enabled():
|
|
|
|
|
return j(handler, {'ok': True, 'message': 'Auth not enabled'})
|
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
|
|
|
client_ip = handler.client_address[0]
|
|
|
|
|
if not _check_login_rate(client_ip):
|
|
|
|
|
return j(handler, {'error': 'Too many attempts. Try again in a minute.'}, status=429)
|
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
|
|
|
password = body.get('password', '')
|
|
|
|
|
if not verify_password(password):
|
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
|
|
|
_record_login_attempt(client_ip)
|
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
|
|
|
return bad(handler, 'Invalid password', 401)
|
|
|
|
|
cookie_val = create_session()
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', 'application/json')
|
|
|
|
|
handler.send_header('Cache-Control', 'no-store')
|
2026-04-03 13:05:41 +00:00
|
|
|
_security_headers(handler)
|
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
|
|
|
set_auth_cookie(handler, cookie_val)
|
|
|
|
|
handler.end_headers()
|
|
|
|
|
handler.wfile.write(json.dumps({'ok': True}).encode())
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if parsed.path == '/api/auth/logout':
|
|
|
|
|
from api.auth import clear_auth_cookie, invalidate_session, parse_cookie
|
|
|
|
|
cookie_val = parse_cookie(handler)
|
|
|
|
|
if cookie_val:
|
|
|
|
|
invalidate_session(cookie_val)
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', 'application/json')
|
|
|
|
|
handler.send_header('Cache-Control', 'no-store')
|
2026-04-03 13:05:41 +00:00
|
|
|
_security_headers(handler)
|
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
|
|
|
clear_auth_cookie(handler)
|
|
|
|
|
handler.end_headers()
|
|
|
|
|
handler.wfile.write(json.dumps({'ok': True}).encode())
|
|
|
|
|
return True
|
|
|
|
|
|
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 False # 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── GET route helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _serve_static(handler, parsed):
|
2026-04-02 06:39:18 +00:00
|
|
|
static_root = (Path(__file__).parent.parent / 'static').resolve()
|
2026-04-02 06:46:40 +00:00
|
|
|
# Strip the leading '/static/' prefix, then resolve and sandbox
|
2026-04-02 06:39:18 +00:00
|
|
|
rel = parsed.path[len('/static/'):]
|
|
|
|
|
static_file = (static_root / rel).resolve()
|
|
|
|
|
try:
|
|
|
|
|
static_file.relative_to(static_root)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return j(handler, {'error': 'not found'}, status=404)
|
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 not static_file.exists() or not static_file.is_file():
|
|
|
|
|
return j(handler, {'error': 'not found'}, status=404)
|
|
|
|
|
ext = static_file.suffix.lower()
|
|
|
|
|
ct = {'css': 'text/css', 'js': 'application/javascript',
|
|
|
|
|
'html': 'text/html'}.get(ext.lstrip('.'), 'text/plain')
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', f'{ct}; charset=utf-8')
|
|
|
|
|
handler.send_header('Cache-Control', 'no-store')
|
|
|
|
|
raw = static_file.read_bytes()
|
|
|
|
|
handler.send_header('Content-Length', str(len(raw)))
|
|
|
|
|
handler.end_headers()
|
|
|
|
|
handler.wfile.write(raw)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_session_export(handler, parsed):
|
|
|
|
|
sid = parse_qs(parsed.query).get('session_id', [''])[0]
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
|
|
|
|
try: s = get_session(sid)
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
payload = json.dumps(s.__dict__, ensure_ascii=False, indent=2)
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', 'application/json; charset=utf-8')
|
|
|
|
|
handler.send_header('Content-Disposition', f'attachment; filename="hermes-{sid}.json"')
|
|
|
|
|
handler.send_header('Content-Length', str(len(payload.encode('utf-8'))))
|
|
|
|
|
handler.send_header('Cache-Control', 'no-store')
|
|
|
|
|
handler.end_headers()
|
|
|
|
|
handler.wfile.write(payload.encode('utf-8'))
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_sessions_search(handler, parsed):
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
q = qs.get('q', [''])[0].lower().strip()
|
|
|
|
|
content_search = qs.get('content', ['1'])[0] == '1'
|
|
|
|
|
depth = int(qs.get('depth', ['5'])[0])
|
|
|
|
|
if not q: return j(handler, {'sessions': all_sessions()})
|
|
|
|
|
results = []
|
|
|
|
|
for s in all_sessions():
|
|
|
|
|
title_match = q in (s.get('title') or '').lower()
|
|
|
|
|
if title_match:
|
|
|
|
|
results.append(dict(s, match_type='title'))
|
|
|
|
|
continue
|
|
|
|
|
if content_search:
|
|
|
|
|
try:
|
|
|
|
|
sess = get_session(s['session_id'])
|
|
|
|
|
msgs = sess.messages[:depth] if depth else sess.messages
|
|
|
|
|
for m in msgs:
|
|
|
|
|
c = m.get('content') or ''
|
|
|
|
|
if isinstance(c, list):
|
|
|
|
|
c = ' '.join(p.get('text', '') for p in c
|
|
|
|
|
if isinstance(p, dict) and p.get('type') == 'text')
|
|
|
|
|
if q in str(c).lower():
|
|
|
|
|
results.append(dict(s, match_type='content'))
|
|
|
|
|
break
|
|
|
|
|
except (KeyError, Exception):
|
|
|
|
|
pass
|
|
|
|
|
return j(handler, {'sessions': results, 'query': q, 'count': len(results)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_list_dir(handler, parsed):
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
sid = qs.get('session_id', [''])[0]
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
2026-04-10 01:18:38 +00:00
|
|
|
try:
|
|
|
|
|
s = get_session(sid)
|
|
|
|
|
workspace = s.workspace
|
|
|
|
|
except KeyError:
|
|
|
|
|
# Fallback for CLI sessions not loaded in WebUI memory
|
|
|
|
|
try:
|
|
|
|
|
cli_meta = None
|
|
|
|
|
for cs in get_cli_sessions():
|
|
|
|
|
if cs['session_id'] == sid:
|
|
|
|
|
cli_meta = cs
|
|
|
|
|
break
|
|
|
|
|
if not cli_meta:
|
|
|
|
|
return bad(handler, 'Session not found', 404)
|
|
|
|
|
workspace = cli_meta.get('workspace', '')
|
|
|
|
|
except Exception:
|
|
|
|
|
return bad(handler, 'Session not found', 404)
|
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:
|
|
|
|
|
return j(handler, {
|
2026-04-10 01:18:38 +00:00
|
|
|
'entries': list_dir(Path(workspace), qs.get('path', ['.'])[0]),
|
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
|
|
|
'path': qs.get('path', ['.'])[0],
|
|
|
|
|
})
|
|
|
|
|
except (FileNotFoundError, ValueError) as e:
|
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
|
|
|
return bad(handler, _sanitize_error(e), 404)
|
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 _handle_sse_stream(handler, parsed):
|
|
|
|
|
stream_id = parse_qs(parsed.query).get('stream_id', [''])[0]
|
|
|
|
|
q = STREAMS.get(stream_id)
|
|
|
|
|
if q is None: return j(handler, {'error': 'stream not found'}, status=404)
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', 'text/event-stream; charset=utf-8')
|
|
|
|
|
handler.send_header('Cache-Control', 'no-cache')
|
|
|
|
|
handler.send_header('X-Accel-Buffering', 'no')
|
|
|
|
|
handler.send_header('Connection', 'keep-alive')
|
|
|
|
|
handler.end_headers()
|
|
|
|
|
try:
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
event, data = q.get(timeout=30)
|
|
|
|
|
except queue.Empty:
|
|
|
|
|
handler.wfile.write(b': heartbeat\n\n')
|
|
|
|
|
handler.wfile.flush()
|
|
|
|
|
continue
|
|
|
|
|
_sse(handler, event, data)
|
|
|
|
|
if event in ('done', 'error', 'cancel'):
|
|
|
|
|
break
|
|
|
|
|
except (BrokenPipeError, ConnectionResetError):
|
|
|
|
|
pass
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_file_raw(handler, parsed):
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
sid = qs.get('session_id', [''])[0]
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
|
|
|
|
try: s = get_session(sid)
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
rel = qs.get('path', [''])[0]
|
|
|
|
|
force_download = qs.get('download', [''])[0] == '1'
|
|
|
|
|
target = safe_resolve(Path(s.workspace), rel)
|
|
|
|
|
if not target.exists() or not target.is_file():
|
|
|
|
|
return j(handler, {'error': 'not found'}, status=404)
|
|
|
|
|
ext = target.suffix.lower()
|
|
|
|
|
mime = MIME_MAP.get(ext, 'application/octet-stream')
|
|
|
|
|
raw_bytes = target.read_bytes()
|
|
|
|
|
import urllib.parse as _up
|
|
|
|
|
safe_name = _up.quote(target.name, safe='')
|
|
|
|
|
handler.send_response(200)
|
|
|
|
|
handler.send_header('Content-Type', mime)
|
|
|
|
|
handler.send_header('Content-Length', str(len(raw_bytes)))
|
|
|
|
|
handler.send_header('Cache-Control', 'no-store')
|
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
|
|
|
# Security: force download for dangerous MIME types to prevent XSS
|
|
|
|
|
dangerous_types = {'text/html', 'application/xhtml+xml', 'image/svg+xml'}
|
|
|
|
|
if force_download or mime in dangerous_types:
|
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
|
|
|
handler.send_header('Content-Disposition',
|
|
|
|
|
f'attachment; filename="{target.name}"; filename*=UTF-8\'\'{safe_name}')
|
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
|
|
|
else:
|
|
|
|
|
handler.send_header('Content-Disposition',
|
|
|
|
|
f'inline; filename="{target.name}"; filename*=UTF-8\'\'{safe_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
|
|
|
handler.end_headers()
|
|
|
|
|
handler.wfile.write(raw_bytes)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_file_read(handler, parsed):
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
sid = qs.get('session_id', [''])[0]
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
|
|
|
|
try: s = get_session(sid)
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
rel = qs.get('path', [''])[0]
|
|
|
|
|
if not rel: return bad(handler, 'path is required')
|
|
|
|
|
try: return j(handler, read_file_content(Path(s.workspace), rel))
|
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 (FileNotFoundError, ValueError) as e: return bad(handler, _sanitize_error(e), 404)
|
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 _handle_approval_pending(handler, parsed):
|
|
|
|
|
sid = parse_qs(parsed.query).get('session_id', [''])[0]
|
|
|
|
|
if has_pending(sid):
|
|
|
|
|
with _lock:
|
|
|
|
|
p = dict(_pending.get(sid, {}))
|
|
|
|
|
return j(handler, {'pending': p})
|
|
|
|
|
return j(handler, {'pending': None})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_approval_inject(handler, parsed):
|
2026-04-02 06:46:40 +00:00
|
|
|
"""Inject a fake pending approval -- loopback-only, used by automated tests."""
|
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
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
sid = qs.get('session_id', [''])[0]
|
|
|
|
|
key = qs.get('pattern_key', ['test_pattern'])[0]
|
|
|
|
|
cmd = qs.get('command', ['rm -rf /tmp/test'])[0]
|
|
|
|
|
if sid:
|
|
|
|
|
submit_pending(sid, {
|
|
|
|
|
'command': cmd, 'pattern_key': key,
|
|
|
|
|
'pattern_keys': [key], 'description': 'test pattern',
|
|
|
|
|
})
|
|
|
|
|
return j(handler, {'ok': True, 'session_id': sid})
|
|
|
|
|
return j(handler, {'error': 'session_id required'}, status=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_output(handler, parsed):
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import OUTPUT_DIR as CRON_OUT
|
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
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
job_id = qs.get('job_id', [''])[0]
|
|
|
|
|
limit = int(qs.get('limit', ['5'])[0])
|
|
|
|
|
if not job_id: return j(handler, {'error': 'job_id required'}, status=400)
|
|
|
|
|
out_dir = CRON_OUT / job_id
|
|
|
|
|
outputs = []
|
|
|
|
|
if out_dir.exists():
|
|
|
|
|
files = sorted(out_dir.glob('*.md'), reverse=True)[:limit]
|
|
|
|
|
for f in files:
|
|
|
|
|
try:
|
|
|
|
|
txt = f.read_text(encoding='utf-8', errors='replace')
|
|
|
|
|
outputs.append({'filename': f.name, 'content': txt[:8000]})
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return j(handler, {'job_id': job_id, 'outputs': outputs})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_recent(handler, parsed):
|
|
|
|
|
"""Return cron jobs that have completed since a given timestamp."""
|
|
|
|
|
import datetime
|
|
|
|
|
qs = parse_qs(parsed.query)
|
|
|
|
|
since = float(qs.get('since', ['0'])[0])
|
|
|
|
|
try:
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import list_jobs
|
|
|
|
|
jobs = list_jobs(include_disabled=True)
|
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
|
|
|
completions = []
|
|
|
|
|
for job in jobs:
|
|
|
|
|
last_run = job.get('last_run_at')
|
|
|
|
|
if not last_run:
|
|
|
|
|
continue
|
|
|
|
|
if isinstance(last_run, str):
|
|
|
|
|
try:
|
|
|
|
|
ts = datetime.datetime.fromisoformat(last_run.replace('Z', '+00:00')).timestamp()
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
ts = float(last_run)
|
|
|
|
|
if ts > since:
|
|
|
|
|
completions.append({
|
|
|
|
|
'job_id': job.get('id', ''),
|
|
|
|
|
'name': job.get('name', 'Unknown'),
|
|
|
|
|
'status': job.get('last_status', 'unknown'),
|
|
|
|
|
'completed_at': ts,
|
|
|
|
|
})
|
|
|
|
|
return j(handler, {'completions': completions, 'since': since})
|
|
|
|
|
except ImportError:
|
|
|
|
|
return j(handler, {'completions': [], 'since': since})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_memory_read(handler):
|
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
|
|
|
try:
|
|
|
|
|
from api.profiles import get_active_hermes_home
|
|
|
|
|
mem_dir = get_active_hermes_home() / 'memories'
|
|
|
|
|
except ImportError:
|
|
|
|
|
mem_dir = Path.home() / '.hermes' / 'memories'
|
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
|
|
|
mem_file = mem_dir / 'MEMORY.md'
|
|
|
|
|
user_file = mem_dir / 'USER.md'
|
|
|
|
|
memory = mem_file.read_text(encoding='utf-8', errors='replace') if mem_file.exists() else ''
|
|
|
|
|
user = user_file.read_text(encoding='utf-8', errors='replace') if user_file.exists() else ''
|
|
|
|
|
return j(handler, {
|
|
|
|
|
'memory': memory, 'user': user,
|
|
|
|
|
'memory_path': str(mem_file), 'user_path': str(user_file),
|
|
|
|
|
'memory_mtime': mem_file.stat().st_mtime if mem_file.exists() else None,
|
|
|
|
|
'user_mtime': user_file.stat().st_mtime if user_file.exists() else None,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── POST route helpers ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
def _handle_sessions_cleanup(handler, body, zero_only=False):
|
|
|
|
|
cleaned = 0
|
|
|
|
|
for p in SESSION_DIR.glob('*.json'):
|
|
|
|
|
if p.name.startswith('_'): continue
|
|
|
|
|
try:
|
|
|
|
|
s = Session.load(p.stem)
|
|
|
|
|
if zero_only:
|
|
|
|
|
should_delete = s and len(s.messages) == 0
|
|
|
|
|
else:
|
|
|
|
|
should_delete = s and s.title == 'Untitled' and len(s.messages) == 0
|
|
|
|
|
if should_delete:
|
|
|
|
|
with LOCK: SESSIONS.pop(p.stem, None)
|
|
|
|
|
p.unlink(missing_ok=True)
|
|
|
|
|
cleaned += 1
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
if SESSION_INDEX_FILE.exists():
|
|
|
|
|
SESSION_INDEX_FILE.unlink(missing_ok=True)
|
|
|
|
|
return j(handler, {'ok': True, 'cleaned': cleaned})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_chat_start(handler, body):
|
|
|
|
|
try: require(body, 'session_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
msg = str(body.get('message', '')).strip()
|
|
|
|
|
if not msg: return bad(handler, 'message is required')
|
|
|
|
|
attachments = [str(a) for a in (body.get('attachments') or [])][:20]
|
|
|
|
|
workspace = str(Path(body.get('workspace') or s.workspace).expanduser().resolve())
|
|
|
|
|
model = body.get('model') or s.model
|
|
|
|
|
s.workspace = workspace; s.model = model; s.save()
|
|
|
|
|
set_last_workspace(workspace)
|
|
|
|
|
stream_id = uuid.uuid4().hex
|
|
|
|
|
q = queue.Queue()
|
|
|
|
|
with STREAMS_LOCK: STREAMS[stream_id] = q
|
|
|
|
|
thr = threading.Thread(
|
|
|
|
|
target=_run_agent_streaming,
|
|
|
|
|
args=(s.session_id, msg, model, workspace, stream_id, attachments),
|
|
|
|
|
daemon=True,
|
|
|
|
|
)
|
|
|
|
|
thr.start()
|
|
|
|
|
return j(handler, {'stream_id': stream_id, 'session_id': s.session_id})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_chat_sync(handler, body):
|
|
|
|
|
"""Fallback synchronous chat endpoint (POST /api/chat). Not used by frontend."""
|
|
|
|
|
from api.config import _get_session_agent_lock
|
|
|
|
|
s = get_session(body['session_id'])
|
|
|
|
|
msg = str(body.get('message', '')).strip()
|
|
|
|
|
if not msg: return j(handler, {'error': 'empty message'}, status=400)
|
|
|
|
|
workspace = Path(body.get('workspace') or s.workspace).expanduser().resolve()
|
|
|
|
|
s.workspace = str(workspace); s.model = body.get('model') or s.model
|
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
|
|
|
from api.streaming import _ENV_LOCK
|
|
|
|
|
with _ENV_LOCK:
|
|
|
|
|
old_cwd = os.environ.get('TERMINAL_CWD')
|
|
|
|
|
os.environ['TERMINAL_CWD'] = str(workspace)
|
|
|
|
|
old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
|
|
|
|
|
old_session_key = os.environ.get('HERMES_SESSION_KEY')
|
|
|
|
|
os.environ['HERMES_EXEC_ASK'] = '1'
|
|
|
|
|
os.environ['HERMES_SESSION_KEY'] = s.session_id
|
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:
|
|
|
|
|
from run_agent import AIAgent
|
|
|
|
|
with CHAT_LOCK:
|
2026-04-02 07:05:09 +00:00
|
|
|
from api.config import resolve_model_provider
|
|
|
|
|
_model, _provider, _base_url = resolve_model_provider(s.model)
|
2026-04-04 20:03:02 +00:00
|
|
|
# Resolve API key via Hermes runtime provider (matches gateway behaviour)
|
|
|
|
|
_api_key = None
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
2026-04-06 21:10:26 +00:00
|
|
|
_rt = resolve_runtime_provider(requested=_provider)
|
2026-04-04 20:03:02 +00:00
|
|
|
_api_key = _rt.get("api_key")
|
|
|
|
|
# Also use runtime provider/base_url if the webui config didn't resolve them
|
|
|
|
|
if not _provider:
|
|
|
|
|
_provider = _rt.get("provider")
|
|
|
|
|
if not _base_url:
|
|
|
|
|
_base_url = _rt.get("base_url")
|
|
|
|
|
except Exception as _e:
|
|
|
|
|
print(f"[webui] WARNING: resolve_runtime_provider failed: {_e}", flush=True)
|
2026-04-02 07:05:09 +00:00
|
|
|
agent = AIAgent(model=_model, provider=_provider, base_url=_base_url,
|
2026-04-04 20:03:02 +00:00
|
|
|
api_key=_api_key, platform='cli', quiet_mode=True,
|
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
|
|
|
enabled_toolsets=CLI_TOOLSETS, session_id=s.session_id)
|
|
|
|
|
workspace_ctx = f"[Workspace: {s.workspace}]\n"
|
|
|
|
|
workspace_system_msg = (
|
|
|
|
|
f"Active workspace at session start: {s.workspace}\n"
|
|
|
|
|
"Every user message is prefixed with [Workspace: /absolute/path] indicating the "
|
|
|
|
|
"workspace the user has selected in the web UI at the time they sent that message. "
|
|
|
|
|
"This tag is the single authoritative source of the active workspace and updates "
|
|
|
|
|
"with every message. It overrides any prior workspace mentioned in this system "
|
|
|
|
|
"prompt, memory, or conversation history. Always use the value from the most recent "
|
|
|
|
|
"[Workspace: ...] tag as your default working directory for ALL file operations: "
|
|
|
|
|
"write_file, read_file, search_files, terminal workdir, and patch. "
|
|
|
|
|
"Never fall back to a hardcoded path when this tag is present."
|
|
|
|
|
)
|
2026-04-04 05:13:12 +00:00
|
|
|
from api.streaming import _sanitize_messages_for_api
|
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
|
|
|
result = agent.run_conversation(
|
|
|
|
|
user_message=workspace_ctx + msg,
|
|
|
|
|
system_message=workspace_system_msg,
|
2026-04-04 05:13:12 +00:00
|
|
|
conversation_history=_sanitize_messages_for_api(s.messages),
|
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
|
|
|
task_id=s.session_id,
|
|
|
|
|
persist_user_message=msg,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
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
|
|
|
with _ENV_LOCK:
|
|
|
|
|
if old_cwd is None: os.environ.pop('TERMINAL_CWD', None)
|
|
|
|
|
else: os.environ['TERMINAL_CWD'] = old_cwd
|
|
|
|
|
if old_exec_ask is None: os.environ.pop('HERMES_EXEC_ASK', None)
|
|
|
|
|
else: os.environ['HERMES_EXEC_ASK'] = old_exec_ask
|
|
|
|
|
if old_session_key is None: os.environ.pop('HERMES_SESSION_KEY', None)
|
|
|
|
|
else: os.environ['HERMES_SESSION_KEY'] = old_session_key
|
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
|
|
|
s.messages = result.get('messages') or s.messages
|
|
|
|
|
s.title = title_from(s.messages, s.title); s.save()
|
2026-04-05 03:07:05 +00:00
|
|
|
# Sync to state.db for /insights (opt-in setting)
|
|
|
|
|
try:
|
|
|
|
|
if load_settings().get('sync_to_insights'):
|
|
|
|
|
from api.state_sync import sync_session_usage
|
|
|
|
|
sync_session_usage(
|
|
|
|
|
session_id=s.session_id,
|
|
|
|
|
input_tokens=s.input_tokens or 0,
|
|
|
|
|
output_tokens=s.output_tokens or 0,
|
|
|
|
|
estimated_cost=s.estimated_cost,
|
|
|
|
|
model=s.model,
|
|
|
|
|
title=s.title,
|
2026-04-07 05:56:27 +00:00
|
|
|
message_count=len(s.messages),
|
2026-04-05 03:07:05 +00:00
|
|
|
)
|
|
|
|
|
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
|
|
|
return j(handler, {
|
|
|
|
|
'answer': result.get('final_response') or '',
|
|
|
|
|
'status': 'done' if result.get('completed', True) else 'partial',
|
|
|
|
|
'session': s.compact() | {'messages': s.messages},
|
|
|
|
|
'result': {k: v for k, v in result.items() if k != 'messages'},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_create(handler, body):
|
|
|
|
|
try: require(body, 'prompt', 'schedule')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try:
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import create_job
|
|
|
|
|
job = create_job(
|
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
|
|
|
prompt=body['prompt'], schedule=body['schedule'],
|
|
|
|
|
name=body.get('name') or None, deliver=body.get('deliver') or 'local',
|
|
|
|
|
skills=body.get('skills') or [], model=body.get('model') or None,
|
|
|
|
|
)
|
|
|
|
|
return j(handler, {'ok': True, 'job': job})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return j(handler, {'error': str(e)}, status=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_update(handler, body):
|
|
|
|
|
try: require(body, 'job_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import update_job
|
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
|
|
|
updates = {k: v for k, v in body.items() if k != 'job_id' and v is not None}
|
2026-04-02 07:05:09 +00:00
|
|
|
job = update_job(body['job_id'], updates)
|
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 not job: return bad(handler, 'Job not found', 404)
|
|
|
|
|
return j(handler, {'ok': True, 'job': job})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_delete(handler, body):
|
|
|
|
|
try: require(body, 'job_id')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import remove_job
|
|
|
|
|
ok = remove_job(body['job_id'])
|
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 not ok: return bad(handler, 'Job not found', 404)
|
|
|
|
|
return j(handler, {'ok': True, 'job_id': body['job_id']})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_run(handler, body):
|
|
|
|
|
job_id = body.get('job_id', '')
|
|
|
|
|
if not job_id: return bad(handler, 'job_id required')
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import get_job
|
|
|
|
|
from cron.scheduler import run_job
|
|
|
|
|
job = get_job(job_id)
|
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 not job: return bad(handler, 'Job not found', 404)
|
2026-04-02 07:05:09 +00:00
|
|
|
threading.Thread(target=run_job, args=(job,), daemon=True).start()
|
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 j(handler, {'ok': True, 'job_id': job_id, 'status': 'triggered'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_pause(handler, body):
|
|
|
|
|
job_id = body.get('job_id', '')
|
|
|
|
|
if not job_id: return bad(handler, 'job_id required')
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import pause_job
|
|
|
|
|
result = pause_job(job_id, reason=body.get('reason'))
|
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 result: return j(handler, {'ok': True, 'job': result})
|
|
|
|
|
return bad(handler, 'Job not found', 404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_cron_resume(handler, body):
|
|
|
|
|
job_id = body.get('job_id', '')
|
|
|
|
|
if not job_id: return bad(handler, 'job_id required')
|
2026-04-02 07:05:09 +00:00
|
|
|
from cron.jobs import resume_job
|
|
|
|
|
result = resume_job(job_id)
|
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 result: return j(handler, {'ok': True, 'job': result})
|
|
|
|
|
return bad(handler, 'Job not found', 404)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_file_delete(handler, body):
|
|
|
|
|
try: require(body, 'session_id', 'path')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
try:
|
|
|
|
|
target = safe_resolve(Path(s.workspace), body['path'])
|
|
|
|
|
if not target.exists(): return bad(handler, 'File not found', 404)
|
|
|
|
|
if target.is_dir(): return bad(handler, 'Cannot delete directories via this endpoint')
|
|
|
|
|
target.unlink()
|
|
|
|
|
return j(handler, {'ok': True, 'path': body['path']})
|
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 (ValueError, PermissionError) as e: return bad(handler, _sanitize_error(e))
|
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 _handle_file_save(handler, body):
|
|
|
|
|
try: require(body, 'session_id', 'path')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
try:
|
|
|
|
|
target = safe_resolve(Path(s.workspace), body['path'])
|
|
|
|
|
if not target.exists(): return bad(handler, 'File not found', 404)
|
|
|
|
|
if target.is_dir(): return bad(handler, 'Cannot save: path is a directory')
|
|
|
|
|
target.write_text(body.get('content', ''), encoding='utf-8')
|
|
|
|
|
return j(handler, {'ok': True, 'path': body['path'], 'size': target.stat().st_size})
|
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 (ValueError, PermissionError) as e: return bad(handler, _sanitize_error(e))
|
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 _handle_file_create(handler, body):
|
|
|
|
|
try: require(body, 'session_id', 'path')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
try:
|
|
|
|
|
target = safe_resolve(Path(s.workspace), body['path'])
|
|
|
|
|
if target.exists(): return bad(handler, 'File already exists')
|
|
|
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
target.write_text(body.get('content', ''), encoding='utf-8')
|
|
|
|
|
return j(handler, {'ok': True, 'path': str(target.relative_to(Path(s.workspace)))})
|
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 (ValueError, PermissionError) as e: return bad(handler, _sanitize_error(e))
|
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 _handle_file_rename(handler, body):
|
|
|
|
|
try: require(body, 'session_id', 'path', 'new_name')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
try:
|
|
|
|
|
source = safe_resolve(Path(s.workspace), body['path'])
|
|
|
|
|
if not source.exists(): return bad(handler, 'File not found', 404)
|
|
|
|
|
new_name = body['new_name'].strip()
|
|
|
|
|
if not new_name or '/' in new_name or '..' in new_name:
|
|
|
|
|
return bad(handler, 'Invalid file name')
|
|
|
|
|
dest = source.parent / new_name
|
|
|
|
|
if dest.exists(): return bad(handler, f'A file named "{new_name}" already exists')
|
|
|
|
|
source.rename(dest)
|
|
|
|
|
new_rel = str(dest.relative_to(Path(s.workspace)))
|
|
|
|
|
return j(handler, {'ok': True, 'old_path': body['path'], 'new_path': new_rel})
|
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 (ValueError, PermissionError, OSError) as e: return bad(handler, _sanitize_error(e))
|
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 _handle_create_dir(handler, body):
|
|
|
|
|
try: require(body, 'session_id', 'path')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
try: s = get_session(body['session_id'])
|
|
|
|
|
except KeyError: return bad(handler, 'Session not found', 404)
|
|
|
|
|
try:
|
|
|
|
|
target = safe_resolve(Path(s.workspace), body['path'])
|
|
|
|
|
if target.exists(): return bad(handler, 'Path already exists')
|
|
|
|
|
target.mkdir(parents=True)
|
|
|
|
|
return j(handler, {'ok': True, 'path': str(target.relative_to(Path(s.workspace)))})
|
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 (ValueError, PermissionError, OSError) as e: return bad(handler, _sanitize_error(e))
|
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 _handle_workspace_add(handler, body):
|
|
|
|
|
path_str = body.get('path', '').strip()
|
|
|
|
|
name = body.get('name', '').strip()
|
|
|
|
|
if not path_str: return bad(handler, 'path is required')
|
|
|
|
|
p = Path(path_str).expanduser().resolve()
|
|
|
|
|
if not p.exists(): return bad(handler, f'Path does not exist: {p}')
|
|
|
|
|
if not p.is_dir(): return bad(handler, f'Path is not a directory: {p}')
|
|
|
|
|
wss = load_workspaces()
|
|
|
|
|
if any(w['path'] == str(p) for w in wss):
|
|
|
|
|
return bad(handler, 'Workspace already in list')
|
|
|
|
|
wss.append({'path': str(p), 'name': name or p.name})
|
|
|
|
|
save_workspaces(wss)
|
|
|
|
|
return j(handler, {'ok': True, 'workspaces': wss})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_workspace_remove(handler, body):
|
|
|
|
|
path_str = body.get('path', '').strip()
|
|
|
|
|
if not path_str: return bad(handler, 'path is required')
|
|
|
|
|
wss = load_workspaces()
|
|
|
|
|
wss = [w for w in wss if w['path'] != path_str]
|
|
|
|
|
save_workspaces(wss)
|
|
|
|
|
return j(handler, {'ok': True, 'workspaces': wss})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_workspace_rename(handler, body):
|
|
|
|
|
path_str = body.get('path', '').strip()
|
|
|
|
|
name = body.get('name', '').strip()
|
|
|
|
|
if not path_str or not name: return bad(handler, 'path and name are required')
|
|
|
|
|
wss = load_workspaces()
|
|
|
|
|
for w in wss:
|
|
|
|
|
if w['path'] == path_str:
|
|
|
|
|
w['name'] = name; break
|
|
|
|
|
else:
|
|
|
|
|
return bad(handler, 'Workspace not found', 404)
|
|
|
|
|
save_workspaces(wss)
|
|
|
|
|
return j(handler, {'ok': True, 'workspaces': wss})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_approval_respond(handler, body):
|
|
|
|
|
sid = body.get('session_id', '')
|
|
|
|
|
if not sid: return bad(handler, 'session_id is required')
|
|
|
|
|
choice = body.get('choice', 'deny')
|
|
|
|
|
if choice not in ('once', 'session', 'always', 'deny'):
|
|
|
|
|
return bad(handler, f'Invalid choice: {choice}')
|
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
|
|
|
# Pop the legacy polling-mode pending entry (no-op when gateway path is active).
|
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
|
|
|
with _lock:
|
|
|
|
|
pending = _pending.pop(sid, None)
|
|
|
|
|
if pending:
|
|
|
|
|
keys = pending.get('pattern_keys') or [pending.get('pattern_key', '')]
|
|
|
|
|
if choice in ('once', 'session'):
|
|
|
|
|
for k in keys: approve_session(sid, k)
|
|
|
|
|
elif choice == 'always':
|
|
|
|
|
for k in keys:
|
|
|
|
|
approve_session(sid, k); approve_permanent(k)
|
|
|
|
|
save_permanent_allowlist(_permanent_approved)
|
fix: surface approval prompt in UI instead of getting stuck in Thinking (#187)
* fix: surface approval prompt in UI instead of getting stuck in Thinking
When a dangerous command was detected during streaming, the approval system
would call submit_pending() but no SSE 'approval' event would be emitted to
the frontend. The agent thread either blocked indefinitely (gateway path) or
returned an approval_required status the UI never saw (EXEC_ASK path). Either
way the chat UI stayed stuck in 'Thinking...' with no prompt shown.
Root cause: streaming.py used HERMES_EXEC_ASK=1 but never registered a
register_gateway_notify() callback. Without it, check_all_command_guards()
fell back to the legacy polling path (submit_pending only), which relies on
on_tool() polling -- but on_tool() fires *before* the tool runs, so by the
time the terminal tool detected the dangerous command and called submit_pending,
the approval event had already missed its window.
Fix (streaming.py):
- Register a gateway-style notify_cb via register_gateway_notify() before the
agent runs. The callback calls put('approval', ...) to emit the SSE event
the moment a dangerous command is detected, regardless of on_tool() timing.
- Unregister via unregister_gateway_notify() in the finally block to unblock
any threads still waiting if the stream ends or is cancelled mid-approval.
- Keep the on_tool() fallback poll for older approval module versions.
Fix (routes.py):
- Import and call resolve_gateway_approval() in _handle_approval_respond().
This unblocks the agent thread parked in entry.event.wait() when the user
clicks Allow or Deny in the UI. Without this call the thread would block
until the 5-minute gateway timeout.
Tests (tests/test_approval_unblock.py):
- 16 new tests covering: resolve_gateway_approval() event signalling, deny/
session/once choices, resolve_all, notify_cb registration/firing/cleanup,
unregister signals blocked entries, full end-to-end streaming simulation,
module symbol exports, and HTTP endpoint regressions.
515 tests pass (499 existing + 16 new).
* feat: full approval UI — i18n buttons, keyboard shortcut, loading state, scoping fix
---------
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 03:16:22 +00:00
|
|
|
# Unblock the agent thread waiting in the gateway approval queue.
|
|
|
|
|
# This is the primary signal when streaming is active — the agent
|
|
|
|
|
# thread is parked in entry.event.wait() and needs to be woken up.
|
|
|
|
|
resolve_gateway_approval(sid, choice, resolve_all=False)
|
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 j(handler, {'ok': True, 'choice': choice})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_skill_save(handler, body):
|
|
|
|
|
try: require(body, 'name', 'content')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
skill_name = body['name'].strip().lower().replace(' ', '-')
|
|
|
|
|
if not skill_name or '/' in skill_name or '..' in skill_name:
|
|
|
|
|
return bad(handler, 'Invalid skill name')
|
|
|
|
|
category = body.get('category', '').strip()
|
2026-04-02 06:46:40 +00:00
|
|
|
if category and ('/' in category or '..' in category):
|
|
|
|
|
return bad(handler, 'Invalid category')
|
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
|
|
|
from tools.skills_tool import SKILLS_DIR
|
|
|
|
|
if category:
|
|
|
|
|
skill_dir = SKILLS_DIR / category / skill_name
|
|
|
|
|
else:
|
|
|
|
|
skill_dir = SKILLS_DIR / skill_name
|
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
|
|
|
# Validate resolved path stays within SKILLS_DIR
|
|
|
|
|
try:
|
|
|
|
|
skill_dir.resolve().relative_to(SKILLS_DIR.resolve())
|
|
|
|
|
except ValueError:
|
|
|
|
|
return bad(handler, 'Invalid skill 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
|
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
skill_file = skill_dir / 'SKILL.md'
|
|
|
|
|
skill_file.write_text(body['content'], encoding='utf-8')
|
|
|
|
|
return j(handler, {'ok': True, 'name': skill_name, 'path': str(skill_file)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_skill_delete(handler, body):
|
|
|
|
|
try: require(body, 'name')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
|
|
|
|
from tools.skills_tool import SKILLS_DIR
|
|
|
|
|
import shutil
|
|
|
|
|
matches = list(SKILLS_DIR.rglob(f'{body["name"]}/SKILL.md'))
|
|
|
|
|
if not matches: return bad(handler, 'Skill not found', 404)
|
|
|
|
|
skill_dir = matches[0].parent
|
|
|
|
|
shutil.rmtree(str(skill_dir))
|
|
|
|
|
return j(handler, {'ok': True, 'name': body['name']})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_memory_write(handler, body):
|
|
|
|
|
try: require(body, 'section', 'content')
|
|
|
|
|
except ValueError as e: return bad(handler, str(e))
|
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
|
|
|
try:
|
|
|
|
|
from api.profiles import get_active_hermes_home
|
|
|
|
|
mem_dir = get_active_hermes_home() / 'memories'
|
|
|
|
|
except ImportError:
|
|
|
|
|
mem_dir = Path.home() / '.hermes' / 'memories'
|
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
|
|
|
mem_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
section = body['section']
|
|
|
|
|
if section == 'memory':
|
|
|
|
|
target = mem_dir / 'MEMORY.md'
|
|
|
|
|
elif section == 'user':
|
|
|
|
|
target = mem_dir / 'USER.md'
|
|
|
|
|
else:
|
|
|
|
|
return bad(handler, 'section must be "memory" or "user"')
|
|
|
|
|
target.write_text(body['content'], encoding='utf-8')
|
|
|
|
|
return j(handler, {'ok': True, 'section': section, 'path': str(target)})
|
|
|
|
|
|
|
|
|
|
|
2026-04-04 02:54:54 +00:00
|
|
|
def _handle_session_import_cli(handler, body):
|
|
|
|
|
"""Import a single CLI session into the WebUI store."""
|
|
|
|
|
try:
|
|
|
|
|
require(body, 'session_id')
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return bad(handler, str(e))
|
|
|
|
|
|
|
|
|
|
sid = str(body['session_id'])
|
|
|
|
|
|
|
|
|
|
# Check if already imported — idempotent
|
|
|
|
|
existing = Session.load(sid)
|
|
|
|
|
if existing:
|
|
|
|
|
return j(handler, {'session': existing.compact() | {
|
|
|
|
|
'messages': existing.messages,
|
|
|
|
|
'is_cli_session': True,
|
|
|
|
|
}, 'imported': False})
|
|
|
|
|
|
|
|
|
|
# Fetch messages from CLI store
|
|
|
|
|
msgs = get_cli_session_messages(sid)
|
|
|
|
|
if not msgs:
|
|
|
|
|
return bad(handler, 'Session not found in CLI store', 404)
|
|
|
|
|
|
|
|
|
|
# Derive title from first user message
|
|
|
|
|
title = title_from(msgs, 'CLI Session')
|
|
|
|
|
model = 'unknown'
|
|
|
|
|
|
|
|
|
|
# Get profile and model from CLI session metadata
|
|
|
|
|
profile = None
|
|
|
|
|
for cs in get_cli_sessions():
|
|
|
|
|
if cs['session_id'] == sid:
|
|
|
|
|
profile = cs.get('profile')
|
|
|
|
|
model = cs.get('model', 'unknown')
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
s = import_cli_session(sid, title, msgs, model, profile=profile)
|
|
|
|
|
s.is_cli_session = True
|
|
|
|
|
s._cli_origin = sid
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {
|
|
|
|
|
'session': s.compact() | {
|
|
|
|
|
'messages': msgs,
|
|
|
|
|
'is_cli_session': True,
|
|
|
|
|
},
|
|
|
|
|
'imported': True,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
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 _handle_session_import(handler, body):
|
|
|
|
|
"""Import a session from a JSON export. Creates a new session with a new ID."""
|
|
|
|
|
if not body or not isinstance(body, dict):
|
|
|
|
|
return bad(handler, 'Request body must be a JSON object')
|
|
|
|
|
messages = body.get('messages')
|
|
|
|
|
if not isinstance(messages, list):
|
|
|
|
|
return bad(handler, 'JSON must contain a "messages" array')
|
|
|
|
|
title = body.get('title', 'Imported session')
|
|
|
|
|
workspace = body.get('workspace', str(DEFAULT_WORKSPACE))
|
|
|
|
|
model = body.get('model', DEFAULT_MODEL)
|
|
|
|
|
s = Session(
|
|
|
|
|
title=title, workspace=workspace, model=model,
|
|
|
|
|
messages=messages,
|
|
|
|
|
tool_calls=body.get('tool_calls', []),
|
|
|
|
|
)
|
|
|
|
|
s.pinned = body.get('pinned', False)
|
|
|
|
|
with LOCK:
|
|
|
|
|
SESSIONS[s.session_id] = s
|
|
|
|
|
SESSIONS.move_to_end(s.session_id)
|
|
|
|
|
while len(SESSIONS) > SESSIONS_MAX:
|
|
|
|
|
SESSIONS.popitem(last=False)
|
|
|
|
|
s.save()
|
|
|
|
|
return j(handler, {'ok': True, 'session': s.compact() | {'messages': s.messages}})
|