Commit Graph

305 Commits

Author SHA1 Message Date
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. <code> becoming <code> 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
nesquena-hermes
80b26c7c72 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-08 20:16:22 -07:00
nesquena-hermes
012ac6f149 docs: v0.40.1 release — default locale fix (#186)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-08 19:35:41 -07:00
nesquena-hermes
18aca24063 fix: default first-install locale to English (#185)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-08 19:35:03 -07:00
nesquena-hermes
a5b843d6f9 docs: v0.40.0 release — i18n, notifications, thinking display (#184)
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-08 19:19:02 -07:00
Nathan Esquenazi
9714c1779f Merge pull request #183 from nesquena/fix/i18n-review-fixes
fix: stray } in message HTML + JS-escape login locale strings
2026-04-08 19:07:29 -07:00
Nathan Esquenazi
0126044ecb fix: stray } in message row HTML + JS-escape login locale strings
Agent review findings from PR #179:

1. static/ui.js line 542: extra } in ternary produced malformed HTML
   in message bubble div (''}} instead of ''}). Caused a literal }
   character to appear in the DOM.

2. api/routes.py: LOGIN_INVALID_PW and LOGIN_CONN_FAILED were inserted
   into JS string context without JS-string escaping. Added backslash
   escaping for ' and \ characters. Currently safe because locale values
   are hardcoded, but this prevents breakage if custom locale strings
   contain single quotes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:07:00 -07:00
Nathan Esquenazi
166a4c3e7b Merge pull request #179 from nesquena/feat/i18n-language-switcher
feat: pluggable i18n with English/Chinese language switcher in Settings
2026-04-08 18:59:11 -07:00
Nathan Esquenazi
1ac1e74512 fix: apply locale to DOM immediately on save — no reload needed
Add applyLocaleToDOM() which walks [data-i18n] elements and re-stamps
their textContent from t(). Called after setLocale() in saveSettings()
so the settings panel labels, checkboxes, and save button update live.
Also called on boot after /api/settings resolves so Chinese persists
without flicker on reload.

- static/i18n.js: add applyLocaleToDOM() function
- static/index.html: add data-i18n attributes to all settings panel
  static text nodes (labels, checkbox spans, save button)
- static/panels.js: call applyLocaleToDOM() + syncTopbar() after save
- static/boot.js: call applyLocaleToDOM() alongside setLocale() on boot
2026-04-08 18:58:20 -07:00
Nathan Esquenazi
b979b4c443 feat: pluggable i18n with English/Chinese language switcher in Settings
Introduces a locale bundle system that makes UI language switchable at
runtime and trivially extensible to any future language.

Architecture:
- static/i18n.js: LOCALES object with 'en' and 'zh' bundles, t(key)
  helper with English fallback, setLocale()/loadLocale() for persistence
  via localStorage. Adding a new language = adding one object.
- api/config.py: 'language' setting (default 'en'), BCP-47 validation
- api/routes.py: _LOGIN_LOCALE dict for server-rendered login page;
  template placeholders substituted at request time from saved setting
- static/index.html: loads i18n.js first (before other scripts); adds
  Language dropdown to Settings panel, auto-populated from LOCALES

Wiring:
- boot.js: applies server-persisted locale at startup (after /api/settings
  fetch); speech recognition lang follows _locale._speech
- panels.js: populates Language dropdown from LOCALES on settings open;
  saves + applies locale on Save Settings
- All JS files: hardcoded user-facing strings replaced with t() calls

Coverage:
- test_sprint20.py: relaxed recognition.lang assertion to accept dynamic
  locale-driven assignment (behavior unchanged for English default)
- 499/499 tests pass

Closes #177 (incorporates Chinese translations as a proper locale bundle
rather than hardcoded strings, so English default is fully preserved)
2026-04-08 18:57:50 -07:00
Nathan Esquenazi
c04caf3f5b Merge pull request #180 from nesquena/feat/notification-sound-browser
feat: notification sound and browser notifications
2026-04-08 18:56:11 -07:00
Nathan Esquenazi
799cbb7eca fix: update sound/notification globals in password branch + close AudioContext
Agent review findings:
- _soundEnabled/_notificationsEnabled not updated in the password-save
  early-return branch of saveSettings() — fixed
- AudioContext never closed after oscillator finishes — added osc.onended
  callback to ctx.close() preventing resource accumulation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:55:57 -07:00
Nathan Esquenazi
0d83837650 Merge pull request #182 from nesquena/fix/thinking-display-edge-cases
fix: harden thinking display streaming edge cases
2026-04-08 18:51:06 -07:00
Nathan Esquenazi
5f7564e8bb fix: harden thinking block streaming display
Hide partial <think> tag prefixes during streaming and rename the local display variable for clarity. References #181.
2026-04-08 18:14:47 +00:00
TaraTheStar
8ff5d83e14 feat: add support for displaying thinking/reasoning blocks in chat 2026-04-08 18:14:09 +00:00
Nathan Esquenazi
5e899ee8fe feat: notification sound and browser notifications on task completion
Add two new settings (both default off):
- sound_enabled: plays a short tone via Web Audio API when assistant
  finishes a response or requests approval
- notifications_enabled: shows a browser notification when a response
  completes while the tab is in the background

Uses Web Audio API (oscillator) instead of bundled MP3 file — zero
additional assets. Follows the standard 4-file settings pattern.

Also skip test_valid_skill_accepted when hermes-agent not installed
(skills endpoint returns 500 without the agent module).

Inspired by #176 (DavidSchuchert)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:02:02 -07:00
Nathan Esquenazi
907bb224d9 Merge pull request #178 from nesquena/fix/streaming-env-lock-deadlock
fix: resolve _ENV_LOCK deadlock that blocks chat after first message
2026-04-08 07:26:53 -07:00
Nathan Esquenazi
d919b584c6 docs: v0.39.1 release notes for ENV_LOCK deadlock fix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 07:26:41 -07:00
Nathan Esquenazi
4422a87de9 fix: resolve _ENV_LOCK deadlock that blocks chat after first message
The v0.39.0 security sprint introduced _ENV_LOCK to protect env var
mutations in the streaming path. The implementation held the lock for
the entire agent run (potentially minutes), then tried to re-acquire
it in the finally block — a guaranteed deadlock on any non-reentrant
threading.Lock().

Result: first message completes (done event fires before finally hits),
but the lock is never released. Every subsequent chat/start POST blocks
forever waiting for that lock.

Fix: narrow the lock scope to just the env mutation. Set the vars inside
the with block, then let the lock release before the agent starts. The
finally block re-acquires cleanly since it no longer re-enters an
already-held lock.

No logic change — only the critical section boundary moves.
2026-04-08 14:22:39 +00:00
nesquena-hermes
9e9fcb09d2 Fix broken link in Quick start section (#175) 2026-04-07 23:39:33 -07:00
nesquena-hermes
12e5de9c4e Refine README for clarity and correctness (#174)
Updated the README to clarify installation steps and improve grammar.
2026-04-07 23:38:44 -07:00
nesquena-hermes
7e6fec1c85 docs: sweep TESTING.md, SPRINTS.md, ROADMAP.md to v0.39.0 / 499 tests
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-07 22:33:08 -07:00
nesquena-hermes
a064542df9 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-07 22:26:03 -07:00