Commit Graph

477 Commits

Author SHA1 Message Date
nesquena-hermes
fc43b897c5 docs: v0.49.1 release notes — Docker docs + mobile Profiles button
- CHANGELOG: entries for #291 (Docker docs) and #265 (mobile Profiles button)
- ROADMAP: sprint table row + header date/count updated to v0.49.1/700
- TESTING.md: test count 700
- SPRINTS.md: version v0.49.1 + test count 700
- static/index.html: version bumped to v0.49.1

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 00:43:45 -07:00
nesquena-hermes
d6a925cf11 feat(mobile): add Profiles button to mobile bottom navigation (#265)
Adds a Profiles button as the last item in the mobile bottom nav bar,
making the Profiles panel reachable on mobile without opening the sidebar.

Fixes from original PR:
- Uses mobileSwitchPanel('profiles') not the broken two-call approach
- data-panel='profiles' attribute present for active-highlight state
- SVG 20x20 stroke-width 1.5 matching all other mobile nav icons
- Placed last (Chat → Tasks → Skills → Memory → Spaces → Profiles)
- 3 new tests in test_mobile_layout.py covering presence, handler, and order

Tests: 700 passed (up from 697)

Co-authored-by: @gabogabucho

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 00:41:52 -07:00
Nathan Esquenazi
5468b04550 docs: two-container Docker setup for Agent + WebUI (#288)
Adds docker-compose.two-container.yml and README section for running
hermes-agent and hermes-webui in separate Docker containers connected
via shared volumes.

The key insight: the WebUI imports hermes-agent's Python modules
directly (not via HTTP), so the agent source must be mounted into
the WebUI container. The existing docker_init.bash handles installing
the agent's dependencies at startup via uv pip install.

Shared volumes:
- hermes-home: config, sessions, skills, memory (~/.hermes)
- hermes-agent-src: agent source code for Python import

Fixes #288

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:37:35 -07:00
nesquena-hermes
7556ea0e04 docs: v0.49.0 final — test count 697, add #287 and #289 entries
All three PRs now merged:
- #285: first-run onboarding wizard
- #287: self-update git pull diagnostics
- #289: skip flaky redaction test in agent-less envs

Final test count: 697 (up from 679)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 00:23:06 -07:00
nesquena-hermes
92fbf2a793 test: skip flaky redaction test in agent-less environments (#289)
This test depends on session state that varies with test ordering.
It passes when run in isolation or with the full hermes agent, but
fails intermittently in the standard test suite. Add to the
auto-skip list alongside other agent-dependent tests.

Fixes #289

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 00:19:36 -07:00
nesquena-hermes
0d98116b37 fix: improve self-update git pull diagnostics (#287)
Rebased and enhanced version of PR #287 by @ccqqlo:

- _run_git() now returns stderr on failure instead of empty string,
  so the UI can surface actionable git error messages
- Added _split_remote_ref() to split tracking refs like origin/master
  into separate remote + branch args for git pull
- Ignore untracked files in stash decision (--untracked-files=no) to
  prevent misleading stash-pop failures
- Fail early with clear message on unresolved merge conflicts
- 4 unit tests covering stderr, stdout fallback, exit code, and ref splitting

Based on work by @ccqqlo in PR #287.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:19:33 -07:00
nesquena-hermes
31a721417e feat(onboarding): add one-shot bootstrap and first-run setup wizard (#285)
Adds a bootstrap launcher and a blocking first-run onboarding wizard that guides
new users through minimum Hermes setup from the browser UI.

Supported provider flows: OpenRouter, Anthropic, OpenAI, custom OpenAI-compatible.
OAuth/terminal-first flows remain via 'hermes model'.

Security hardening applied during review:
- /api/onboarding/setup restricted to loopback when auth disabled
- Newline injection guard in _write_env_file
- esc() on setup.unsupported_note in onboarding.js
- Test isolation fix (send_key instead of bot_name in contamination test)
- Skip markers for PyYAML-dependent tests in agent-less environments

Tests: 693 passed (up from 679)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: gabogabucho <gabogabucho@gmail.com>
2026-04-12 00:11:41 -07:00
nesquena-hermes
f9663d2f1d docs: bump to v0.49.0 — onboarding wizard release
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 23:41:10 -07:00
nesquena-hermes
cbc3c01604 docs: v0.48.2 release notes — provider mismatch warning
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 21:26:34 -07:00
nesquena-hermes
42dd2b562d fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266)

Fixes #266 — WebUI silently ignores provider/model selection mismatch.

The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while
Hermes is configured for a different provider (e.g. local Ollama) sends
the request to the wrong endpoint, which returns a 401 Unauthorized error
with no UI indication of why.

Three-layer fix:

1. api/streaming.py — detect 401/auth errors explicitly
   Added is_auth_error detection covering '401', 'AuthenticationError',
   'authentication', 'unauthorized', 'invalid api key', and the specific
   Ollama error string 'no cookie auth credentials'. Auth errors emit
   apperror with type='auth_mismatch' and a hint pointing to 'hermes model'.

2. static/ui.js — expose active_provider and warn on selection
   - populateModelDropdown() stores data.active_provider from /api/models
     as window._activeProvider (the field was already in the response but
     the frontend never used it)
   - New _checkProviderMismatch(modelId) helper: compares the selected
     model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against
     the active provider. Skips the check for 'openrouter' and 'custom'
     to avoid false positives on configs that legitimately route any model.

3. static/boot.js — warn on model dropdown change
   modelSelect.onchange calls _checkProviderMismatch() and shows a toast
   when the selected model looks incompatible with the configured provider.

4. static/messages.js — distinct UI label for auth errors
   apperror handler now distinguishes type='auth_mismatch' and shows
   'Provider mismatch' as the error label instead of 'Error'.

5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label
   keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant).

Tests: 21 new tests in tests/test_provider_mismatch.py covering all
five change areas. 679/679 total pass (658 baseline + 21 new).

* fix: t() call args spread + use i18n label for auth mismatch

1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single
   array arg to t(). Since t(key, ...args) spreads, the function
   received the array as m and undefined as p. Fixed to pass as
   separate args: t('provider_mismatch_warning', modelId, ap).

2. messages.js: 'Provider mismatch' label was hardcoded instead of
   using t('provider_mismatch_label'). Now uses the i18n key with
   fallback for when t() isn't available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:25:18 -07:00
nesquena-hermes
6b4ff53315 docs: v0.48.1 release notes — table inline formatting
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 21:03:51 -07:00
nesquena-hermes
ce84d1bafa fix: render inline formatting in markdown table cells (#273)
Table cells used esc() which escaped all HTML including <strong>,
<em>, <code> tags. Changed to inlineMd() which processes markdown
bold/italic/code/links and allows safe HTML tags through.

This runs after the pre-pass that converts <strong> to ** and
<em> to *, so both HTML tags and markdown syntax in table cells
are rendered correctly.

Fixes #273

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:03:01 -07:00
nesquena-hermes
afa540a222 docs: v0.48.0 release notes — gateway session sync
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 20:54:21 -07:00
nesquena-hermes
711bb5a6c9 feat: real-time gateway session sync (Phase 1) (#274)
* feat: add real-time gateway session sync (Phase 1)

- Add gateway_watcher.py: background daemon polling state.db every 5s
  for gateway session changes (telegram, discord, slack, etc.)
- Extend get_cli_sessions() to include all non-webui sources
- Add SSE endpoint /api/sessions/gateway/stream for real-time push
- Add dynamic source badges (telegram=blue, discord=purple, slack=dark purple)
- Rename 'Show CLI sessions' to 'Show agent sessions'
- Wire watcher lifecycle into server start/stop
- 10 tests covering metadata, filtering, SSE, and watcher lifecycle
- Activated via the same checkbox as CLI session import

Addresses GitHub issue #272

* fix: SSE event name mismatch, TLS attribute, remove PLAN.md

- Fix critical SSE bug: frontend listened for 'gateway_session_update'
  but backend sends 'sessions_changed' -- events were silently dropped
- Fix frontend field check: data.changed -> data.sessions (matches
  the actual payload structure from gateway_watcher)
- Fix TLS: ssl.TLSv1_2 -> ssl.TLSVersion.TLSv1_2 (the bare attribute
  does not exist, would crash TLS setup and silently fall back to HTTP)
- Remove PLAN.md: implementation plan should not be committed to repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: test isolation and slow-consumer sentinel in gateway sync

tests/test_gateway_sync.py:
- Fix _get_test_state_dir() path mismatch: the function was computing
  HERMES_HOME/webui-mvp-test but conftest.py sets HERMES_HOME=TEST_STATE_DIR,
  so state.db was written to a double-nested path the server never read.
  Now uses HERMES_WEBUI_STATE_DIR first (which conftest sets directly to
  TEST_STATE_DIR), fixing the 7/10 test failures in full-suite ordering.
- Fix conn cleanup: removed conn.close() from inside try blocks so the
  connection stays valid for _remove_test_sessions() in the finally block.
  Previously the closed conn caused ProgrammingError in finally (swallowed
  by bare except), leaving ghost sessions in state.db on test failure.

api/gateway_watcher.py:
- Fix slow-consumer queue eviction: when a subscriber queue fills (>10 events)
  and is removed from _subscribers, now puts a None sentinel into it so the
  SSE handler unblocks and closes the connection, letting EventSource
  auto-reconnect. Without this the connection stayed open but received no
  further events.

* fix: test isolation — set HERMES_WEBUI_TEST_STATE_DIR in conftest

The gateway sync tests write directly to state.db and must use the same
path the test server reads from.  Previously they computed the path
independently, which broke when test_auth_sessions.py set a different
HERMES_WEBUI_STATE_DIR in the test-process environment at import time.

tests/conftest.py:
- Set HERMES_WEBUI_TEST_STATE_DIR=TEST_STATE_DIR in the test process's
  os.environ (via setdefault) so gateway tests can read it reliably.
  Using setdefault preserves any explicit override the caller may pass.

tests/test_gateway_sync.py:
- Simplify _get_test_state_dir(): check HERMES_WEBUI_TEST_STATE_DIR first
  (now reliably set by conftest), fall back to HERMES_HOME/webui-mvp-test.
  Remove the workaround that tried to snapshot HERMES_HOME at import time.

Result: 658/658 tests pass in full-suite ordering (was 651 pass / 7 fail).

---------

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:53:12 -07:00
nesquena-hermes
c677893105 docs: v0.47.1 release notes — Spanish locale
- CHANGELOG: v0.47.1 entry for Spanish locale (PR #275)
- ROADMAP: header updated v0.47.0 → v0.47.1, 645 → 648 tests; sprint row added
- TESTING.md: test count 645 → 648
- static/index.html: version v0.47.0 → v0.47.1

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 20:08:34 -07:00
nesquena-hermes
eca6f5efbd feat(i18n): add Spanish locale for WebUI (#275)
* feat(i18n): add Spanish locale for WebUI

* fix(i18n): translate tab_skills to Habilidades in Spanish locale

tab_skills was left as 'Skills' (English) in the es block — the only
sidebar tab that wasn't translated. Changed to 'Habilidades', the correct
Spanish term for Skills.

Also added tab_skills and tab_memory to the representative translation
assertions in test_spanish_locale.py to lock this in for future changes.

---------

Co-authored-by: gabogabucho <gabogabucho@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 20:06:37 -07:00
nesquena-hermes
068836cf6b fix: add aria-label to mobile workspace panel close button
The × button added for the mobile workspace panel close in v0.47.0
had a title= attribute but no aria-label. Screen readers may announce
the raw × character ('times' or 'multiplication sign') instead of
reading the title. Added aria-label='Close workspace panel' to match
the accessibility pattern used by other icon buttons in the panel header.

All 645 tests pass.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 16:18:46 -07:00
nesquena-hermes
09325f1bdf docs: rewrite HERMES.md with accurate 2026 market comparisons
* docs: rewrite HERMES.md with accurate 2026 market comparisons

* fix: correct /loop and scheduling claims for Claude Code

Three factual errors corrected:

1. /loop is a native bundled skill available without any plugin. The doc
   incorrectly described it as behavior from the ralph-wiggum plugin.
   ralph-wiggum provides /ralph-loop, which is distinct: it iterates toward
   a completion goal. /loop polls on a fixed schedule. Both exist and serve
   different purposes.

2. claude.ai/code/scheduled is not a real usable URL or scheduling interface.
   Removed the reference. Cloud scheduling is described as cloud-managed cron
   with a 1-hour minimum interval.

3. 'your data leaves your hardware' was only half-true. Desktop scheduled tasks
   run locally with full file access. Cloud tasks do leave your hardware. Rewrote
   to be precise: the real distinction vs Hermes cron is that neither option runs
   as a headless server daemon.

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 15:45:38 -07:00
nesquena-hermes
1003fa410c docs: add CSS icon hotfix note to v0.47.0 CHANGELOG
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 12:39:12 -07:00
nesquena-hermes
a2ae953620 fix: session action menu icon aligned left of text (not above it)
The .ws-opt base class uses flex-direction:column which was causing the
SVG icon to stack above the label+meta text in each session action menu
item. Added row-flex layout to .session-action-opt .ws-opt-action,
removed inherited padding from .session-action-opt (moved to the inner
action span), and gave .ws-opt-icon a fixed width:16px + flex-shrink:0
so icons stay left-aligned regardless of text length.

Each menu item now shows: [icon] | Title\nSubtitle — one row per option.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 12:28:56 -07:00
nesquena-hermes
b86ace6ce3 v0.47.0: dialogs, session menu, /skills, mobile fixes, mobile QA suite
* fix: custom provider with slash model name no longer rerouted to OpenRouter (#255)

When base_url is configured in config.yaml, resolve_model_provider() now
trusts the configured provider/base_url entirely and skips the slash-based
OpenRouter heuristic. Fixes google/gemma-4-26b-a4b with provider:custom
being silently routed to OpenRouter, resulting in 401 errors.

Fixes #230

* test: mobile layout regression suite — 14 tests for every QA run (#254)

Adds tests/test_mobile_layout.py with 14 static regression tests that run
on every QA pass to catch mobile layout breakage before it reaches prod.
Covers: breakpoints at 900px/640px, right panel slide-over CSS, mobile
overlay, bottom nav, files button, profile dropdown z-index, chip overflow,
workspace close, 100dvh, 44px touch targets, 16px font-size on textarea.

* feat: /skills slash command lists and filters available Hermes skills (#257)

Adds /skills [query] command to commands.js. Fetches from /api/skills,
groups by category (alphabetically sorted), displays as a formatted
assistant message. Optional query filters by name, description, or category.
i18n keys added for en, de, zh, zh-Hant. 1 regression test added.

Fixes #248

* feat: shared app dialogs replace native confirm()/prompt() calls (#251)

Adds showConfirmDialog() and showPromptDialog() helpers to ui.js, backed
by a themed #appDialogOverlay. Replaces all 11 native browser confirm/prompt
call sites across panels.js, sessions.js, ui.js, workspace.js.

Supports: danger mode, keyboard focus trap (Tab/Escape/Enter), focus restore,
ARIA roles, mobile-responsive stacked buttons at 640px. i18n for en/de/zh/zh-Hant.
5 new tests in test_sprint33.py verify markup, CSS, helpers, and absence of
native dialog calls.

Extracted from PR #242.

* fix: Android Chrome mobile — workspace panel close + profile dropdown (#256)

Fix #247: toggleMobileFiles() now shows/hides the mobile overlay when
toggling the right workspace panel. New closeMobileFiles() helper closes
the panel with correct overlay state tracking. Overlay onclick calls both
closeMobileSidebar() and closeMobileFiles(). Mobile-only close button (x)
added to workspace panel header.

Fix #246: profile dropdown uses position:fixed;top:56px;right:8px at
max-width:900px, escaping the overflow-x:auto stacking context that was
clipping it on Android Chrome.

Fix applied during review: closeMobileSidebar() now checks if the right
panel is still open before hiding the overlay, preventing the overlay from
disappearing when only the sidebar is closed.

Fixes #247 Fixes #246

* feat: session ⋯ action dropdown replaces per-row buttons (#252)

Replaces the 5 per-row hover action buttons (pin/move/archive/duplicate/trash)
with a single ⋯ trigger that opens a positioned dropdown menu. Menu has full
keyboard (Escape), click-outside, scroll, and resize-reposition handling.
Position:fixed prevents sidebar clipping.

5 actions: Pin/Unpin, Move to project, Archive/Unarchive, Duplicate, Delete
(danger style). Each with icon and descriptive subtitle.

Updated test_sprint16.py: test_sessions_js_uses_action_menu_not_per_row_buttons
asserts the new trigger and menu functions exist, old per-row classes are gone.

Extracted from PR #242.

* docs: v0.47.0 release notes, bump version, update test counts (645)

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 12:19:12 -07:00
nesquena-hermes
c357ed9b74 docs: fix stale TESTING.md footer (v0.36/440 -> v0.46.0/624)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 10:27:12 -07:00
nesquena-hermes
27c2fd6c08 v0.46.0: security, Docker UID/GID, model discovery, i18n, cancel fix
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239)

Adds decode() helper in renderMd() to fix double-escaping of HTML entities
from LLM output (e.g. &lt;code&gt; becoming &amp;lt;code&amp;gt; instead
of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns.

Also adds 40+ missing zh (Simplified Chinese) translation keys and a new
zh-Hant (Traditional Chinese) locale with 163 keys.

Fix applied: removed duplicate settings_label_notifications key in both
zh and zh-Hant locales.

Fixes #240

* fix: restore custom model list discovery with config api key (#238)

get_available_models() now reads api_key from config.yaml before env vars:
  1. model.api_key
  2. providers.<active>.api_key / providers.custom.api_key
  3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.)

Also adds OpenAI/Python User-Agent header and a regression test covering
authenticated /v1/models discovery.

Fixes users with LM Studio / Ollama custom endpoints configured in
config.yaml whose model picker silently collapsed to the default model.

* feat: Docker UID/GID matching to avoid root-owned .hermes files (#237)

Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so
container files match the host user UID/GID. Prevents .hermes volume
mounts from being owned by root when using a non-root host user.

Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000).
Readme updated with setup instructions.

Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml
that was overriding the ${GID:-1000} variable expansion.

* security: redact credentials from API responses and fix credential file permissions (#243)

Adds response-layer credential redaction to three endpoints:
  - GET /api/session — messages[], tool_calls[], and title
  - GET /api/session/export — download also redacted
  - SSE done event — session payload in stream
  - GET /api/memory — MEMORY.md and USER.md content

Adds api/startup.py with fix_credential_permissions() at server startup.
Adds 13 tests in tests/test_security_redaction.py.

Merged with #237 container detection changes in server.py.

* fix: cancel button now interrupts agent and cleans up UI state (#244)

Wires agent.interrupt() into cancel_stream() so the backend actually
stops tool execution when the user clicks Cancel, rather than only
stopping the SSE stream while the agent keeps running.

Changes:
  - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent)
  - api/streaming.py: stores agent in AGENT_INSTANCES after creation,
    checks CANCEL_FLAGS immediately after store (race condition fix),
    calls agent.interrupt() in cancel_stream(), cleans up in finally block
  - static/boot.js: removes stale setStatus(cancelling) call
  - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel

Race condition fix: after storing agent in AGENT_INSTANCES, immediately
checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during
agent init) and interrupts before starting. Check is inside the same
STREAMS_LOCK acquisition, making it atomic.

New test file: tests/test_cancel_interrupt.py with 6 unit tests.

* docs: v0.46.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-11 10:17:52 -07:00
nesquena-hermes
0e112455ec fix: stale test count in ROADMAP.md header (499 -> 604)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 11:55:18 -07:00
nesquena-hermes
02e6e768e6 docs: v0.45.0 release notes + roadmap/sprint plan updates
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 11:47:04 -07:00
nesquena-hermes
da160d675f feat: custom endpoint fields in new profile form (fixes #170, closes #214)
* feat: add custom endpoint fields to new profile form

* fix: skip config write tests when PyYAML not installed

The 4 unit tests for _write_endpoint_to_config imported yaml directly
without handling ImportError. Added pytest.importorskip('yaml') at
module level so the entire test class skips cleanly in environments
without PyYAML. Removed redundant per-method yaml imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: wire frontend for custom endpoint fields in new profile form

- Add Base URL and API key inputs to the profile create form (index.html)
- Wire panels.js submitProfileCreate() to send base_url and api_key
- Clear new fields on form toggle/cancel
- Add client-side URL format validation (must start with http:// or https://)
- Add server-side URL format validation in routes.py (400 for invalid scheme)
- Add test_api_route_rejects_invalid_base_url() covering the new validation
- Base URL input has placeholder 'http://localhost:11434' per review suggestion

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:43:49 -07:00
nesquena-hermes
1e27940535 docs: v0.44.1 release notes, version bump, correct test counts
- static/index.html: v0.44.0 → v0.44.1
- CHANGELOG.md: add [v0.44.1] entry for unskip fix; clarify v0.44.0 test
  count was 579/595 at that tag (595/595 after unskip)
- TESTING.md: 579 passing, 16 skipped → 595 passing, 0 skipped

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 10:56:37 -07:00
nesquena-hermes
2215aced19 fix: unskip 16 approval tests — remove stale has_pending/pop_pending imports
These two symbols were removed from tools.approval when the agent renamed
has_pending -> has_blocking_approval (checks gateway queue, not _pending dict)
and dropped pop_pending. They were only in the import block — never used in
any test body — but their absence caused the entire file to skip via the
APPROVAL_AVAILABLE guard.

Before: 595 collected, 579 passed, 16 skipped
After:  595 collected, 595 passed, 0 skipped

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 10:10:28 -07:00
nesquena-hermes
4947a6b0c3 v0.44.0: approval fix, login CSP, update diagnostics, Lucide icons
* fix: approval pending check broken by stale has_pending import (#228)

api/routes.py imported has_pending/pop_pending from tools.approval, but the
agent module renamed has_pending to has_blocking_approval (checks gateway
queue, not _pending dict) and removed pop_pending. The import fell through
to fallback lambdas that always returned False, making GET /api/approval/pending
always return {pending:null} even after a successful inject_test.

Fix: check _pending directly under _lock — same dict submit_pending writes to.
Stale imports removed.

Before: 554 pass, 1 fail | After: 555 pass, 0 fail

* fix: move login JS into external file, remove inline handlers (#226)

Login page used inline onsubmit/onkeydown handlers and an inline <script>
block — all blocked by strict script-src CSP, causing silent login failure.

Fix: extract doLogin() and Enter key listener into static/login.js (served
from /static/, already a public path). Form uses id='login-form' and
data-* attributes for i18n strings instead of injected JS literals.
Also guards res.json() parse with try/catch so non-JSON error bodies
(e.g. HTTP 500) show the password-error fallback instead of 'Connection failed'.

Fixes #222.

* fix: improve update error messages when pull fails (#227)

_apply_update_inner() ran git pull --ff-only and returned only raw stderr
on failure, making all failure modes indistinguishable.

Fix: explicit git fetch before pull; if fetch fails, returns human-readable
network error. Diverged history and missing upstream tracking branch each
get distinct messages with exact recovery commands. Generic fallback
truncates to 300 chars and shows sentinel when git produces no output.

Also adds tests/test_update_checker.py with 13 tests covering all 4 new
diagnostic code paths (0 tests existed before).

Fixes #223.

* fix: stabilize 30s terminal approval prompt visibility (#225)

Adds minimum 30-second visibility guard for the approval card using
_approvalVisibleSince, _approvalHideTimer, and a signature fingerprint
to deduplicate repeated poll ticks.

Fix: respondApproval() and all stream-end paths (done/cancel/apperror/
error/start-error) now call hideApprovalCard(true) so the card hides
immediately when the user responds or the session ends. The 30s guard
only applies to mid-session poll ticks where the approval is still live
but briefly absent.

Adds 11 structural tests covering the new timer variables, force
parameter, force-on-respond, force-on-stream-end, and poll-loop
no-force behavior.

* feat: replace emoji icons with self-hosted Lucide SVG icons (#221)

Replaces all sidebar/button emoji icons with SVG paths from Lucide bundled
in static/icons.js (no CDN dependency). Adds li(name) function returning
inline SVG geometry from a hardcoded whitelist — unknown keys return '' so
dynamic server-supplied names never inject arbitrary SVG.

Changes:
  - static/icons.js: new file with 21 icon paths + li() renderer
  - static/index.html: all nav/action buttons now use li() icons
  - static/ui.js: toolIcon(), fileIcon() use li() for tool/file icons
  - static/messages.js: cancelStream button uses SVG square stop icon
  - .gitignore: adds node_modules/ entry

Verified: all 35 onclick= functions exist in JS, all 21 li() calls
reference defined icons, applyBotName() selectors intact, version
label present, no removed IDs referenced by JS.

* docs: v0.44.0 release notes, bump version, update test counts

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 10:02:28 -07:00
nesquena-hermes
0df9d4830f docs: v0.43.1 — CSRF reverse proxy fix (#220)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 01:27:09 -07:00
Nathan Esquenazi
e0a95193d8 fix: CSRF check supports reverse proxy headers (#218) (#219)
Tests pass: 20/21 QA suite (1 known skip), all browser API sanity checks green, CSRF fix verified end-to-end.
2026-04-10 01:24:18 -07:00
nesquena-hermes
e3c85624d9 docs: v0.43.0 release — auto-install agent deps, session ID validator, test suite isolation fix (571 tests) (#217)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 01:10:02 -07:00
nesquena-hermes
ed9023a431 fix: wire auto_install_agent_deps into server.py startup (#216)
* fix: wire auto_install_agent_deps into server.py startup; add api/startup.py to ARCHITECTURE.md

* fix(tests): kill stale process on test port before server start in conftest

Stale servers left by QA harness runs (ports 8792/8793 etc.) or prior
test sessions could interfere with conftest starting its own server on
TEST_PORT (8788). If the port was already occupied, _wait_for_server
hit the wrong server and tests got unexpected 404s/500s, failing
non-deterministically — the 'conftest isolation issue' seen this session.

Fix: run fuser -k on TEST_PORT before launching the new server process,
with a 0.5s sleep for port release. The full suite now runs 571/571
reliably regardless of what other servers were previously active.

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 00:56:07 -07:00
nesquena-hermes
e59fedd351 feat: auto-install missing agent deps on startup (#215)
* feat: auto-install missing agent deps on startup

* fix: patch HERMES_HOME in test_skips_when_agent_dir_missing to prevent real agent fallback

The test patched HERMES_WEBUI_AGENT_DIR to a nonexistent path but left
HERMES_HOME unpatched. In the full test suite HERMES_HOME resolves to the
real hermes agent dir, causing the fallback in _agent_dir() to find and
use it — making auto_install_agent_deps() call pip instead of returning
False. Fix: also patch HERMES_HOME to a nonexistent dir in env_overrides.

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-10 00:42:02 -07:00
nesquena-hermes
9a5435176d fix: broaden session ID validator to support new hermes-agent format (#212)
* fix: broaden session ID validator to support new hermes-agent format

* test: add more path traversal evil IDs to session validator test

Add null byte, backslash, forward slash, and dot-extension variants
to the rejected session ID test to cover additional attack vectors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:00:02 -07:00
nesquena-hermes
31281a6025 docs: v0.42.2 release — CSP unsafe-inline fix (564 tests) (#210)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 19:08:30 -07:00
nesquena-hermes
cc8cbc4d3f fix(security): add unsafe-inline and CDN allowlist to CSP script-src (#209)
The CSP script-src 'self' policy blocked all inline onclick= event handlers
in index.html (55+ handlers including toggleSettings(), switchPanel(),
filterSessions() etc.), making the settings panel, sidebar navigation, and
most interactive UI elements non-functional.

Also restores https://cdn.jsdelivr.net to both script-src and style-src
(required for Mermaid.js dynamic load in ui.js and Prism.js static load
in index.html). This was present in the original PR #197 merge but was
dropped in the v0.42.1 commit.

script-src additions:
- 'unsafe-inline': required for onclick=/oninput=/onchange= attributes
- https://cdn.jsdelivr.net: Mermaid (dynamic) and Prism (static with SRI)

style-src: retains 'unsafe-inline' + cdn.jsdelivr.net (Prism CSS)

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 19:07:51 -07:00
nesquena-hermes
0e5e465ea0 fix: i18n button text stripping and German translation corrections (v0.42.1)
Three sidebar buttons (+ New job/skill/profile) and three suggestion
buttons had data-i18n on the outer element, causing applyLocaleToDOM
to strip the + prefix and emoji characters when switching locales.
Fixed by wrapping only the label text in a <span data-i18n=...>.

Also corrects German translations:
- cancelling: imperative -> progressive (Wird abgebrochen...)
- editing: first-person verb -> noun (Bearbeitung)
- empty_subtitle: add missing 'explore files' clause
- settings_desc_check_updates: add git fetch detail
- settings_desc_cli_sessions: add 'continue the conversation' clause

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 19:04:48 -07:00
nesquena-hermes
a92e21553d docs: v0.42.0 release — German i18n, custom provider routing, phantom Custom group fix (564 tests) (#207)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 18:44:04 -07:00
David Schuchert
06f46439c0 feat: add German translation and make UI elements translatable (#190)
Co-authored-by: David Work <davidwork@MBP-von-David.fritz.box>
2026-04-09 18:35:23 -07:00
nesquena-hermes
e68c1b92a4 fix: do not build phantom Custom group when active provider is set (#206)
* fix: do not build phantom "Custom" group when active provider is set

When model.provider is a real provider (e.g. openai-codex) and model.base_url
is configured, hermes_cli reports 'custom' as an authenticated provider. The
WebUI model picker was building a separate "Custom" group for it and parking
the configured default_model there instead of under the active provider's
group — diverging from the TUI which correctly shows the model under its
configured provider.

Two fixes in api/config.py get_available_models():

1. Discard 'custom' from detected_providers when active_provider is set and
   isn't 'custom' itself. The base_url belongs to the active provider.

2. Replace the substring-based default-model injection check with an exact
   match against _PROVIDER_DISPLAY. The old check `active_provider.lower() in
   g.get('provider', '').lower()` silently failed for hyphenated IDs like
   'openai-codex' vs display name 'OpenAI Codex' (hyphen vs. space),
   falling through to groups[0] and landing the model in the alphabetical
   first group instead.

Adds two regression tests in tests/test_model_resolver.py covering both
conditions.

* fix: do not build phantom Custom group when active provider is set

Two bugs in get_available_models():

1. Phantom Custom group: hermes_cli reports 'custom' as authenticated
whenever model.base_url is set. With provider=openai-codex + base_url,
detected_providers contained both 'openai-codex' and 'custom', producing
a duplicate group. Fixed by discarding 'custom' from detected_providers
when the active provider is any real named provider.

2. Hyphen/space mismatch in default_model injection: the substring check
'openai-codex' in 'openai codex' is False (hyphen vs space), causing the
default model to fall through to groups[0] (alphabetically first provider)
instead of the active provider group. Fixed by using _PROVIDER_DISPLAY
for exact display-name comparison.

Also fixes test helper _available_models_with_full_cfg to clear model env
vars during the call, preventing real hermes profile env from leaking into
the test assertions.

---------

Co-authored-by: mbac <marco.baciarello@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 18:33:24 -07:00
sean
fb19c7ea1f fix: route slash-based custom provider models correctly (#189)
Co-authored-by: smurmann <smurmann@users.noreply.github.com>
2026-04-09 18:23:40 -07:00
nesquena-hermes
cb069794dd docs: v0.41.0 release — TLS, CSP, session memory leak, slow-client timeout, update checker, CLI file browser (561 tests) (#205)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-09 18:20:07 -07:00
Cyprian Kowalczyk
be92e59bdb fix: support CLI sessions in /api/list file browser (#204)
* feat: optional HTTPS/TLS support via cert and key env vars

Add optional HTTPS support controlled by two env vars:
  HERMES_WEBUI_TLS_CERT=/path/to/cert.pem
  HERMES_WEBUI_TLS_KEY=/path/to/key.pem

- Wraps server socket with ssl.SSLContext (min TLSv1.2)
- Dynamic scheme detection for startup messages (http:// vs https://)
- Graceful fallback to HTTP if cert loading fails — server never crashes
  due to bad TLS config, just prints a warning and continues
- Auth cookie Secure flag already set when HTTPS is detected via getpeercert
- 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works,
  fallback on bad paths

Addresses #191 (HTTPS support issue).

* fix: use current branch upstream for update checks, not repo default branch

The update checker in api/updates.py always compared HEAD against
origin/master (or origin/main), which produced false 'N updates
available' alerts when the user is on a feature branch and master has
moved forward with unrelated commits.

Now uses git rev-parse --abbrev-ref @{upstream} to get the current
branch's tracking branch for both the behind-count check and the
apply-update pull command. Falls back to the default branch if no
upstream is set (brand-new local branch with no tracking config).

Fixes #200.

* fix: support CLI sessions in /api/list file browser

_handle_list_dir() only checked WebUI in-memory sessions, returning
'Session not found' for CLI sessions imported from the agent's state.db.
Now falls back to get_cli_sessions() to find the workspace path for
CLI sessions that aren't loaded in WebUI memory.

Fixes: workspace pane showing empty for CLI sessions.
2026-04-09 18:18:38 -07:00
Cyprian Kowalczyk
f90be60e31 fix: use current branch upstream for update checks instead of default branch (#201)
* feat: optional HTTPS/TLS support via cert and key env vars

Add optional HTTPS support controlled by two env vars:
  HERMES_WEBUI_TLS_CERT=/path/to/cert.pem
  HERMES_WEBUI_TLS_KEY=/path/to/key.pem

- Wraps server socket with ssl.SSLContext (min TLSv1.2)
- Dynamic scheme detection for startup messages (http:// vs https://)
- Graceful fallback to HTTP if cert loading fails — server never crashes
  due to bad TLS config, just prints a warning and continues
- Auth cookie Secure flag already set when HTTPS is detected via getpeercert
- 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works,
  fallback on bad paths

Addresses #191 (HTTPS support issue).

* fix: use current branch upstream for update checks, not repo default branch

The update checker in api/updates.py always compared HEAD against
origin/master (or origin/main), which produced false 'N updates
available' alerts when the user is on a feature branch and master has
moved forward with unrelated commits.

Now uses git rev-parse --abbrev-ref @{upstream} to get the current
branch's tracking branch for both the behind-count check and the
apply-update pull command. Falls back to the default branch if no
upstream is set (brand-new local branch with no tracking config).

Fixes #200.
2026-04-09 18:10:11 -07:00
Cyprian Kowalczyk
011034dc71 feat: optional HTTPS/TLS support via cert and key env vars (#199)
Add optional HTTPS support controlled by two env vars:
  HERMES_WEBUI_TLS_CERT=/path/to/cert.pem
  HERMES_WEBUI_TLS_KEY=/path/to/key.pem

- Wraps server socket with ssl.SSLContext (min TLSv1.2)
- Dynamic scheme detection for startup messages (http:// vs https://)
- Graceful fallback to HTTP if cert loading fails — server never crashes
  due to bad TLS config, just prints a warning and continues
- Auth cookie Secure flag already set when HTTPS is detected via getpeercert
- 6 end-to-end tests: config flags, HTTPS handshake, HTTP still works,
  fallback on bad paths

Addresses #191 (HTTPS support issue).
2026-04-09 18:08:29 -07:00
Cyprian Kowalczyk
392bc5df6e fix: add Content-Security-Policy and Permissions-Policy headers (#197)
Add CSP and Permissions-Policy headers to _security_headers() for
defense-in-depth against XSS and unwanted browser feature access.

CSP policy:
  default-src 'self' — only load resources from same origin
  script-src 'self' — prevent inline/remote script injection
  style-src 'self' 'unsafe-inline' — allow themes (inline styles)
  img-src 'self' data: — allow workspace images and data URIs
  font-src 'self' data: — allow web fonts
  connect-src 'self' — only allow fetch/XHR to same origin
  base-uri 'self'; form-action 'self' — prevent base/form injection

Permissions-Policy: disable camera, microphone, geolocation.

Addresses #193.
2026-04-09 18:07:07 -07:00
Cyprian Kowalczyk
fdf6ebfbe6 fix(auth): prune expired sessions on every verify to prevent memory leak (#196)
* fix(auth): prune expired sessions on every verify to prevent memory leak

The in-memory _sessions dict accumulated expired tokens indefinitely —
entries were only removed when that specific token was verified. Add a
lazy _prune_expired_sessions() call at the top of verify_session() so
all expired entries are swept during normal traffic.

Addresses #192.

* test(auth): add 8 unit tests for session lifecycle and lazy pruning

Tests verify:
- Fresh session creation and validation
- Expired entries are pruned during verify_session() calls
- Valid sessions are never removed by pruning
- Empty dict is safe for pruning
- Session TTL matches expected 24-hour window
- invalidate_session() actually removes the token
- Invalidating non-existent tokens is safe
2026-04-09 18:05:23 -07:00
Cyprian Kowalczyk
04678b7b6e feat(server): add 30s connection timeout to prevent slow-client thread exhaustion (#198)
Set Handler.timeout = 30. Python's BaseHTTPRequestHandler.setup()
calls self.request.settimeout(timeout), which raises socket.timeout
on idle or slow connections after the configured duration.

This defends against Slowloris-style attacks where a client holds
connections open indefinitely, exhausting threads in ThreadingHTTPServer.
Also recovers threads from crashed clients with hung TCP connections.

Addresses #194.
2026-04-09 18:05:18 -07:00
nesquena-hermes
4d68fb31d4 docs: v0.40.2 release — approval UI, 547 tests (#188)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-08 20:17:14 -07:00