The previous logic treated a blank password field as intent to clear auth,
which meant saving any other setting (model, send key, etc.) would silently
disable password protection.
New behavior:
- Blank password field + Save Settings = no change to auth (do nothing)
- Password field with content + Save = set/change password (unchanged)
- 'Disable Auth' button = explicit confirmation-gated clear (new)
UI changes:
- index.html: updated description text to 'Leave blank to keep current
setting'; added 'Disable Auth' button (amber, shown only when auth active)
- panels.js: saveSettings() skips password logic entirely when field is blank;
loadSettingsPanel() shows/hides both btnDisableAuth and btnSignOut based on
auth_enabled; new disableAuth() function sends _clear_password:true after
confirm() prompt and hides both auth buttons on success
Server: no logic changes needed; _clear_password handling in save_settings()
is now only triggered by the explicit Disable Auth action.
'/' and '/index.html' were in PUBLIC_PATHS, so setting a password
and refreshing the root URL would show the app blank (JS loaded
but all API calls returned 401) instead of redirecting to /login.
Root and index.html must be protected paths so the browser gets a
302 -> /login when auth is active and no valid session cookie exists.
Three security issues found during review:
1. password_hash exposed via GET /api/settings
load_settings() returned all fields including the stored hash.
Fix: strip password_hash from the response in routes.py.
2. password_hash directly settable via POST /api/settings
'password_hash' was in _SETTINGS_ALLOWED_KEYS, so an attacker
could POST {password_hash: 'X'} to hijack auth without knowing
the current password.
Fix: exclude password_hash from _SETTINGS_ALLOWED_KEYS.
(Use _set_password for the legitimate hash-and-store path.)
3. Security headers missing from /api/auth/login and /api/auth/logout
These endpoints built their responses manually (bypassing j()),
so they omitted X-Content-Type-Options etc.
Fix: call _security_headers() before end_headers() on both.
Tests updated: renamed test to assert key absent (not just None),
added new test verifying direct password_hash POST is blocked.
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>
- Fix stale tree cache: clear _dirCache and _expandedDirs on root nav
- Fix clearPreview: prompt before discarding unsaved preview edits
- Update UI version label from v0.17.1 to v0.20
- Add Sprint 18 entry to CHANGELOG.md
- Update SPRINTS.md current state to v0.20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- File preview auto-close: clearPreview() extracted as named function
and called from loadDir(). Navigating directories (breadcrumbs, up
button, folder clicks) now automatically closes the right panel
file preview instead of leaving stale content visible.
- Thinking/reasoning display: assistant messages with structured content
arrays containing type=thinking or type=reasoning blocks now render
as collapsible gold-themed cards above the response text. Collapsed
by default, click header to expand. Works with Claude extended thinking
and o3 reasoning tokens when preserved in the message array.
- Workspace tree view (Issue #22): directories expand/collapse in-place
with toggle arrows. Single-click toggles, double-click navigates
(breadcrumb view). Subdirectory contents fetched lazily and cached.
Indentation shows nesting depth. Empty directories show "(empty)".
S._expandedDirs tracks open state, S._dirCache caches fetched entries.
Tests: 295 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CHANGELOG.md and SPRINTS.md incorrectly stated 294 tests for v0.19.
Actual count is 318 (289 from v0.18.1 + 6 new Sprint 17 + 23 in
test_regressions.py). The Sprint 17 commit message miscounted as
'5 new' when test_sprint17.py contains 6 tests.
Also adds a Tests section to the Sprint 17 CHANGELOG entry listing
what the 6 tests cover, and notes the send_key enum validation.
Tests: 318 passed, 0 failed.
Track A: Workspace breadcrumb navigation
- Breadcrumb path bar with clickable segments when inside subdirectories
- Up button in panel header for parent directory navigation
- S.currentDir state tracking; file ops stay in current directory
- New file/folder creation respects current subdirectory
Track B: Slash commands foundation
- New commands.js module (7th JS module) with command registry and parser
- Built-in commands: /help, /clear, /model, /workspace, /new
- Autocomplete dropdown on / input with arrow/tab/enter/escape navigation
- Unrecognized commands pass through to agent normally
Track C: Send key setting (closes#26)
- send_key added to settings defaults in api/config.py
- Settings panel dropdown: Enter (default) vs Ctrl/Cmd+Enter
- Keydown handler rewritten for autocomplete + send key preference
- Setting loaded on boot, persisted to settings.json
5 new tests, 242 total (219 passing, 22 pre-existing failures, 0 regressions).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CHANGELOG: add v0.18.1 entry (safe HTML rendering, inlineMd, safety
net, active session gold style, 74 new tests)
- ARCHITECTURE: update ui.js line count (809->846), document renderMd
pre-pass/safety net/inlineMd/SAFE_TAGS, update test file count (14),
update Phase I test count (289)
- ROADMAP: bump version and test count
- SPRINTS: bump version, test count, Sprint 16 test total
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
renderMd() now correctly renders safe inline HTML tags that AI models
emit in their responses:
Pre-pass (ui.js):
Converts <strong>, <b>, <em>, <i>, <code>, <br> to their markdown
equivalents (**text**, *text*, `text`, newline) before the pipeline
runs. Code blocks and backtick spans are stashed first so their content
is never modified.
inlineMd() helper (ui.js):
New helper for processing inline formatting inside list items,
blockquotes, and headings. Previously these used esc() directly, which
escaped <strong>/<code> tags that had already been converted from HTML
by the pre-pass — causing them to appear as literal <strong> text
instead of rendering as bold. inlineMd() applies bold/italic/code
processing and then escapes only unknown tags.
Safety net (ui.js):
After the full pipeline, any HTML tags NOT emitted by our own renderer
(i.e. <img>, <script>, <iframe>, <svg>, <object>, etc.) are escaped
via esc(). The SAFE_TAGS allowlist covers every tag the pipeline itself
produces. XSS is fully blocked.
Active session gold style (sessions.js, style.css):
Active session item now uses gold/amber (#e8a030) instead of blue,
matching the logo gradient color for better visual hierarchy.
Project color border-left is skipped when the session is active
(gold always wins). Session items get border-radius: 0 8px 8px 0
to complement the left border indicator.
Tests (tests/test_sprint16.py — 74 tests):
- Static analysis: pre-pass, SAFE_TAGS, SAFE_INLINE, inlineMd present
- Behavioural: all safe tags render in paragraphs, list items (ul+ol),
blockquotes, headings (h1/h2/h3)
- Exact screenshot regression: the 4-item list with <strong> labels
and <code> values that was showing as literal text
- XSS: 7 attack vectors blocked (<img>, <script>, <iframe>, <svg>,
<object>, XSS inside bold, XSS nested inside <strong>)
- Edge cases: code block protection, double-escaping guards, br tag,
mixed markdown+HTML, inlineMd called in list/blockquote handlers
Tests: 312 passed, 0 failed.
Brings all documentation up to date after Sprint 16, PRs #18-25:
- CHANGELOG: add v0.18 (Sprint 16), v0.17.3 (bug fixes), update footer
- ROADMAP: Sprint 16 in history, custom model discovery feature, updated
line counts and architecture table, fix Wave 3 checkboxes
- SPRINTS: bump version to v0.18, update parity percentages, fix
reasoning display sprint reference
- ARCHITECTURE: update all file line counts to match current source,
add Session model fields (pinned, archived, project_id, tool_calls),
document ICONS constant and sessions.js overlay pattern, update
Phase A/I sections
- BUGS: restructure with open/fixed sections, add v0.17.3 fixes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Action buttons overlay: wrap pin/move/archive/dup/trash in a
.session-actions container with position:absolute. Titles now use
full available width. Actions appear on hover with gradient fade
from the right edge. Overlay auto-hides during inline rename.
- SVG line icons: replace all emoji HTML entities with monochrome
SVGs that inherit currentColor. Consistent across all platforms.
- Pin indicator: small gold star rendered inline only when pinned.
Unpinned sessions get full title width (zero space reservation).
- Project border: sessions assigned to a project show a colored
left border matching the project color, replacing the old
always-visible blue folder button.
Fixes both BUGS.md items (title truncation + sticky folder button).
Tests: 214 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_available_models() references 'logger' in the except block of
the custom endpoint fetch (added in PR #18), but 'logger' is never
imported or defined in api/config.py. When the custom endpoint is
unreachable (the normal case -- most users don't have a local LLM),
the except handler raises NameError: name 'logger' is not defined,
which propagates as a 500 on every GET /api/models request.
This broke 7 test_sprint11 tests and caused the model dropdown to
fail for all users regardless of whether they have a custom endpoint.
Fix: replace logger.debug() with a silent pass -- the exception is
expected when no local LLM is configured and needs no logging.
Tests: 237 passed, 0 failed.
Five fixes to the Sprint 15 Move to Project picker:
1. CRITICAL: Picker was invisible (overflow:hidden clipping)
Appended to document.body + positioned with fixed/getBoundingClientRect
instead of inside .session-item (overflow:hidden). Flips above button
when near bottom of viewport.
2. CRITICAL: Picker stretched full screen width
position:fixed removed the containing block width constraint. Added
max-width:220px; width:max-content to .project-picker.
3. UX: No way to create a project from the picker
Added '+ New project': creates project and moves session in one click.
4. UX: Feature was undiscoverable
Folder button shows persistently (blue, 60% opacity) when session
has a project.
5. Minor: Event listener leak
removeEventListener was missing from picker item onclick handlers.
Tests: 237 passed (7 pre-existing failures from unrelated logger bug).
Adds the newly available GLM-5.1 model to the hardcoded Z.AI provider
model list so it appears in the model dropdown. Fixes#17.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert routes.py and streaming.py to master: resolve_model_provider()
already handles provider routing and base_url passthrough for all models.
- Fix indentation error in config.py (2-space indent on comment line).
- Fix auto_detected_models scope: initialize before try block.
- Remove unused urllib.parse import.
- Simplify unknown-provider model group logic.
- Remove verbose comments and redundant variable assignments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Picker dropdown: append to document.body with fixed positioning instead
of inside the session-item (which has overflow:hidden). Flips above
when near bottom of viewport.
- Add "+ New project" item at bottom of picker so users can create a
project and assign in one flow.
- Folder button stays visible (blue, 60% opacity) when session belongs
to a project, instead of only appearing on hover.
- Clean up document click listener in all picker item onclick handlers
to prevent stale listener accumulation.
Tests: 214 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- resolve_model_provider: fix regression where OpenRouter model IDs like
openai/gpt-5.4-mini had their prefix stripped, causing AIAgent to look
for OPENAI_API_KEY (direct API) instead of routing through OpenRouter.
All chats returned Connection lost for OpenRouter users. Fix: only strip
prefix and use direct-API when config.provider explicitly matches that
provider; pass full provider/model string through for openrouter.
- Project name: cap at 128 chars, reject empty after strip on create/rename
- Project color: validate ^#[0-9a-fA-F]{3,8}$ to prevent CSS injection
via dot.style.background in sessions.js
- Remove 2 redundant sys.path.insert() calls in cron handlers
Tests: 214 passed, 23 pre-existing failures, 0 regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session projects: named groups for organizing sessions. Project filter
bar with chips between search and session list. Create/rename/delete
projects, assign sessions via folder icon dropdown. Stored in
projects.json, project_id on Session model. 5 new API endpoints.
Code block copy button: every code block gets a Copy button in the
language header (or top-right for plain blocks). Clipboard API with
"Copied!" feedback.
Tool card expand/collapse: messages with 2+ tool cards get an
"Expand all / Collapse all" toggle above the card group.
13 new tests (237 total), all passing. No regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Restore resolve_model_provider() in _handle_chat_sync -- removed
multi-provider model routing, breaking cross-provider selection.
2. Restore new URL(path, location.origin) + credentials:include on
fetch calls -- reverted reverse-proxy auth fix from v0.16.1.
3. Revert cron import refactor (_cron_module, _real_hermes_home_env)
back to original from cron.jobs import pattern.
Tests: 201 passed, 23 pre-existing failures, 0 new regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Path traversal in _serve_static() [CRITICAL]
Sandbox resolved path to static/ directory using relative_to().
GET /static/../../../../etc/passwd now returns 404.
2. Skill category path traversal [HIGH]
Validate category param in skill save: reject values with '/' or '..'.
3. Gate /api/approval/inject_test to loopback only [HIGH]
Endpoint now returns 404 for any non-127.0.0.1 client,
preserving test functionality while blocking remote access.
4. Escape captured groups in renderMd() [HIGH]
All inline markdown regexes (bold, italic, headings, blockquote,
list items, table cells/headers, link labels) now run captured
text through esc() before inserting into innerHTML, preventing
XSS via AI-generated content.
5. SRI hashes for CDN resources + pin Mermaid version [MEDIUM]
Added integrity= + crossorigin= to all three PrismJS CDN tags.
Pinned Mermaid from floating @10 to @10.9.3 with SRI hash.
Tests: 224 passed, 0 failed.
Resolved path was not checked against the static/ directory, allowing
GET /static/../../../../etc/passwd to serve arbitrary files.
Fix: resolve the path and call relative_to(static_root) before serving.
Returns 404 for any path that escapes the static/ directory.
fix(css): add !important to three dead mobile overrides in @media(640px)
Three @media(max-width:640px) rules added by the mobile responsive PR
were silently overridden by later bare rules in the same stylesheet:
.composer-wrap padding (overridden by line 347)
.suggestion-grid max-width (overridden by line 364)
.tool-card margin-left (overridden by line 460)
Fix: add !important to these three declarations so the mobile overrides
actually fire on narrow screens.
Tests: 224 passed, 0 failed.
- Update _PROVIDER_MODELS['minimax'] from stale ABAB 6.5 models to
current MiniMax-M2.7/M2.5/M2.1 lineup (matching hermes-agent upstream)
- Update _PROVIDER_MODELS['zai'] from GLM-4 to current GLM-5/4.7/4.5
lineup (matching hermes-agent upstream)
- Extend resolve_model_provider() to also return base_url from config.yaml,
so providers with custom endpoints (MiniMax, Z.AI) are routed correctly
- Pass base_url to AIAgent in both streaming and sync chat paths
Fixes#6
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add entry for mobile responsive layout, reverse proxy auth support,
and model provider routing fixes contributed by @deboste.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>