Commit Graph

1162 Commits

Author SHA1 Message Date
Harlan Zhou e8b426d825 test: avoid global env-coupled defaults regression 2026-05-25 01:58:32 +00:00
Harlan Zhou f8a7726e09 fix(windows): align WebUI defaults with Hermes Agent home path 2026-05-25 01:58:32 +00:00
nesquena-hermes 7e22c4edd5 Merge pull request #2894 — send Joplin token in Authorization header
# Conflicts:
#	CHANGELOG.md
2026-05-25 01:47:23 +00:00
nesquena-hermes 15dc7373c1 Merge pull request #2895 — preserve cached agent prefill context
# Conflicts:
#	CHANGELOG.md
2026-05-25 01:47:23 +00:00
Frank Song dc86841547 fix: send joplin token in auth header 2026-05-25 08:51:44 +08:00
Frank Song 535c238285 fix: preserve cached agent prefill context 2026-05-25 08:51:44 +08:00
Frank Song 826e719a29 fix: validate auxiliary model task slots 2026-05-25 08:51:44 +08:00
nesquena-hermes 1c2d574882 Stage-batch14: Opus advisor SHOULD-FIX patches (UX + defense-in-depth)
Inline fixes for 4 of 5 Opus SHOULD-FIX items before tag:

1. /api/auth/status now gates passkeys_enabled / passwordless_enabled on
   _passkey_feature_flag_enabled() — when flag is off, status reports
   no credentials even if passkeys.json has legacy entries. New
   passkey_feature_flag field added to the response for the frontend.

2. Settings → System Passkeys block (passkeysSettingsBlock) now starts
   display:none and loadPasskeys() reveals it only when the server
   confirms passkey_feature_flag === true AND /api/auth/passkeys
   doesn't return {disabled: true}. Stops the broken-affordance trap
   where users would see Add passkey → click → 404.

3. /api/settings/save now refuses to set passwordless mode when the
   passkey feature flag is off. Closes the auth-bypass path Opus flagged:
   user goes passwordless while flag on → admin unsets flag → restart
   serves the WebUI fully unauthenticated.

4. CHANGELOG entries added for PR #2685 (replayed-context dedup +
   per-turn metering cap) and PR #2824 (Stop server affordance,
   relocated to Settings) — both PRs had functional changes but no
   release-notes entries. Also enriched the rate-limit detail on the
   #2739 entry (30 events / 60s / 4KB body cap).

Deferred to follow-up issue (#5 in Opus review):
- Live tool metering cumulative cap across many tool calls — non-trivial
  refactor of _bump_live_prompt_estimate, will be a separate PR
2026-05-25 00:26:40 +00:00
nesquena-hermes 46ed70bfde Stage-batch14: add HERMES_WEBUI_PASSKEY feature flag for #2859 passkey support
Per the stage-batch14 ship plan, passkey/WebAuthn support is shipped
opt-in default-off behind an explicit feature flag so deployments can
disable the entire surface (UI + endpoints + credential storage) without
needing to delete code.

Enable via either:
  - HERMES_WEBUI_PASSKEY=1 environment variable, OR
  - webui_passkey_enabled: true in config.yaml

With the flag off:
  - are_passkeys_enabled() returns False even if credentials exist
  - is_auth_enabled() falls back to password-only checking
  - /login renders password-only (no passkey button)
  - All 6 /api/auth/passkey/* endpoints return 404 with a clear message
  - Settings → System → Passkeys section is hidden

Mirrors the #2527 notes-drawer flag shape (env-or-config, truthy parse).
Auth is high-stakes; opt-in lets us land the code while keeping default
deployments on the well-tested password-only path.

Touches: api/auth.py (new _passkey_feature_flag_enabled helper, gated
are_passkeys_enabled), api/routes.py (6 endpoint guards).
2026-05-25 00:16:12 +00:00
AJV20 1b48643f63 feat: support passkey-only auth 2026-05-25 00:14:38 +00:00
AJV20 c60ff543b5 feat: add passkey sign-in 2026-05-25 00:14:38 +00:00
gavinssr 0ab3ad3bb2 fix: place shutdown route after CSRF gate
Move POST /api/shutdown routing after the CSRF check so drive-by
cross-origin requests cannot bring down a dev server with auth off.
Also replace os._exit(0) with os.kill(os.getpid(), signal.SIGINT)
so atexit handlers and pending session writes run during shutdown.
2026-05-25 00:10:52 +00:00
gavinssr 39121650d4 feat: add shutdown button to WebUI title bar
Add a power button (⏻) in the title bar that gracefully stops the
WebUI server process from the browser.

- api/routes.py: POST /api/shutdown endpoint with threaded os._exit(0)
- static/boot.js: shutdownServer() with confirm prompt, BroadcastChannel
  cross-tab notification, and _showServerStopped() placeholder UI
- static/index.html: shutdown button HTML in title bar (after reload btn)
- static/style.css: .app-titlebar-shutdown styles, hover turns red
2026-05-25 00:10:52 +00:00
Lumen Yang d0992730a9 fix: preserve repeated state rows in replay delta 2026-05-25 00:10:27 +00:00
Lumen Yang 5934c2fe8a fix: address context replay review feedback 2026-05-25 00:10:27 +00:00
Lumen Yang c616c8e788 fix: cap live tool prompt estimates 2026-05-25 00:10:27 +00:00
Lumen Yang 50c69713cc fix: reconcile state db delta after context 2026-05-25 00:10:27 +00:00
Lumen Yang 15cde132f3 fix: dedupe replayed context summaries 2026-05-25 00:10:27 +00:00
ai-ag2026 8a2f11c770 fix(chat): log sanitized client sse diagnostics
(cherry picked from commit 749ca6e18c5e307fbf7e7fb5fffce97249545017)
2026-05-25 02:06:42 +02:00
ai-ag2026 2f1ca959f1 fix(chat): classify interrupted response causes
(cherry picked from commit 5c1e802cd6ee8565da74c7ffe57e6407fe21bf02)
2026-05-25 02:06:42 +02:00
ai-ag2026 efe3d7c296 fix(chat): avoid false restart wording for interrupted responses
(cherry picked from commit ef8fd879682aeb729a7b7afa1e7c46478ca5ebb6)
2026-05-25 02:06:42 +02:00
nesquena-hermes e5533ea0e4 Merge pull request #2547 from AJV20/fix/webui-context-parity
fix(chat): align WebUI context with messaging sessions
2026-05-24 23:12:27 +00:00
nesquena-hermes 767a9cd06d Merge pull request #2527 from AJV20/feat/webui-notes-sources
feat(memory): show third-party notes sources
2026-05-24 23:11:54 +00:00
AJV20 d7b98d87cd Merge remote-tracking branch 'origin/master' into maint/pr-2547
# Conflicts:
#	CHANGELOG.md
2026-05-24 17:48:09 -04:00
AJV20 24979c8af1 Merge remote-tracking branch 'origin/master' into maint/pr-2527
# Conflicts:
#	CHANGELOG.md
2026-05-24 17:48:09 -04:00
AJV20 6e2991f45a Merge remote-tracking branch 'origin/master' into maint/pr-2865
# Conflicts:
#	CHANGELOG.md
2026-05-24 17:48:06 -04:00
hermes-agent 7796f74ca3 Stage 406: Opus MUST-FIX — parity-fix sync_session_usage profile= at api/routes.py:9007
Opus pre-release advisor caught a #2762 parity gap. api/streaming.py:5078
(_run_agent_streaming worker, background thread) correctly passes
profile= to sync_session_usage post-#2827. But the SECOND production
call site at api/routes.py:9007 (_handle_chat_sync, HTTP thread) does
not. Safe TODAY (HTTP thread sets TLS correctly), but it's a
defense-in-depth gap: anyone wrapping that handler in a worker pool
later silently regresses the fix. Closes the parity gap so the
threat-model invariant holds regardless of future threading changes.
2026-05-24 19:39:28 +00:00
AJV20 7af7370be6 Merge remote-tracking branch 'origin/master' into fix/session-personality-default
# Conflicts:
#	CHANGELOG.md
2026-05-24 15:05:29 -04:00
hermes-agent fd1c4eaeaf Stage 406: PR #2827 — fix(state-sync): pass profile explicitly so background-thread DB writes hit the right state.db (#2762) by @Koraji95-coder 2026-05-24 18:57:40 +00:00
AJV20 9bd595de40 fix: avoid stamping display personality on sessions 2026-05-24 14:57:37 -04:00
hermes-agent 7222095413 Stage 405: Opus MUST-FIX + Stamp CHANGELOG for v0.51.129 (Release DA / stage-batch11 / 4-PR feature + perf batch)
Opus pre-release advisor MUST-FIX patched inline:
- api/routes.py:7290-7308 _handle_folder_download: add Connection: close
  header before end_headers() to satisfy HTTP/1.1 framing on the on-the-fly
  ZIP stream. Without it, post-#2836 protocol_version bump leaves clients
  hanging waiting for the next pipelined response after central-directory
  bytes finish. Opus verified this is the ONLY streaming response #2836
  missed — all other paths (j/t helpers, 12 hand-written responses, 8 SSE
  endpoints, auth flow) are already correctly framed.
2026-05-24 18:52:54 +00:00
hermes-agent a86b378036 Stage 405: PR #2680 — feat: add Auxiliary Models settings card by @mccxj
Cherry-picked via 3-way apply (rebase had failed on static/index.html
conflict when applied via rebase commit chain; 3-way of the net delta
against stage HEAD applied cleanly).

Co-authored-by: mccxj <mccxj@github.users.noreply.github.com>
2026-05-24 18:28:26 +00:00
Qi 598fd4ff83 perf(http): enable HTTP/1.1 keep-alive
Enable HTTP/1.1 on the WebUI server so browsers can reuse TCP
connections across normal API/static requests. Tighten response framing
by adding Content-Length to short manual responses and marking
SSE/streaming responses as Connection: close, keeping HTTP/1.1 message
boundaries unambiguous.

Verified:
- python3 -m py_compile server.py api/auth.py api/routes.py api/kanban_bridge.py
- pytest tests/test_auth_*.py tests/test_*sse*.py tests/test_pr1350_*.py
        tests/test_pr1355_sse_handler_no_deadlock.py tests/test_kanban_bridge.py
        tests/test_logs_ui_static.py tests/test_onboarding_static.py
        tests/test_regressions.py tests/test_1038_pwa_auth_redirect.py
        tests/test_issue1623_sse_heartbeat_alignment.py
  → 239 passed, 1 skipped
2026-05-24 18:26:56 +00:00
Michael Lam dd7648d56c feat(runtime): wire runner route selection harness 2026-05-24 18:26:55 +00:00
hermes-agent 2419b3a0a2 Stage 404: PR #2830 — fix(sessions): keep pin state authoritative by @franksong2702 (closes #2821)
Agent reviewer 'LGTM. Ship it.'
- Bug A fix: _session_field helper handles dict-vs-object snapshot in pin-limit check
- Bug B fix: removed stale client-side pinLimitReached short-circuit
- Bug C recovery: renderSessionList() on pin/unpin failure refreshes from server

Co-authored-by: franksong2702 <146128127+franksong2702@users.noreply.github.com>
2026-05-24 18:08:42 +00:00
hermes-agent 9d95ba0b92 Stage 404: PR #2716 — Performance optimizations by @dobby-d-elf
nesquena APPROVED 2026-05-22. Cherry-picked onto post-v0.51.127
master via 3-way apply. Resolved api/routes.py conflict: master had
the inline correctness fix from the deep-review iteration; PR
refactors it into _metadata_only_message_summary() helper. Took the
helper AND added profile= threading (post-#2827 master adds
profile-aware state.db reads). Kept master's pre-existing
test_api_session_reload_drops_stale_cached_user_tail_after_saved_assistant
alongside the PR's new test_metadata_fast_path_matches_reconciliation_for_restamped_replays.

Co-authored-by: dobby-d-elf <dobby.the.agent@gmail.com>
2026-05-24 18:08:41 +00:00
hermes-agent 130be3db1d Stage 403: Opus pre-release fixes (1 MUST-FIX + 3 SHOULD-FIX)
MUST-FIX:
- tests/test_2735_open_in_vscode.py: bump expected open_in_vscode locale
  counter from 10 to 11 (Turkish locale added in #2772). The bump fell
  out of an in-rebase test edit but never got committed; tagging without
  this would have shipped a failing test in the release commit.

SHOULD-FIX inline:
- api/updates.py: case-D drift in _select_apply_compare_ref. The original
  #2855 fix used latest_tag in the past-tag predicate; the check side
  uses current_tag (HEAD's nearest reachable tag) plus a 'behind == 0'
  gate. They drift when HEAD is on an OLDER release tag with commits on
  top AND a NEWER tag exists ('case D'): check correctly suggests
  advancing to the newer tag, but apply fell through to origin/<branch>.
  Mirror the check-side predicate exactly. Adds regression test
  test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_exists.
- static/messages.js: post-await race guard in _restoreSettledSession.
  stream_end without preceding 'done' enters the settlement path, awaits
  /api/session, then sets _streamFinalized=true. If a late 'done' event
  arrives during that await, it sees _streamFinalized still false and
  double-runs the finalize. The guard returns early when done won the
  race, avoiding double renderMessages() + double notification.
- server.py: CORS preflight Access-Control-Allow-Methods now includes PUT.
  #2776 wired PUT into the router for /api/mcp/servers/{name} but didn't
  update the OPTIONS response. Same-origin only in practice, but cosmetic
  completeness for CORS-aware deployments.

Opus advisor verdict: all 5 risk areas reviewed, 1 MUST-FIX + 3 SHOULD-FIX
all addressed inline. Net: +69/-9, no new architecture, no behavior risk.
2026-05-24 17:42:06 +00:00
Uğur Murat Altıntas c77936ff81 feat(i18n): add Turkish (tr) locale support
Add a complete Turkish locale to the WebUI and login page so users can
select Türkçe in Settings, with speech recognition via tr-TR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 17:13:34 +00:00
Rory Ford 7be9a26018 feat: PATCH /api/mcp/servers/{name} — enable/disable toggle
Add `PATCH /api/mcp/servers/{name}` endpoint that accepts `{"enabled": bool}`,
updates `mcp_servers.<name>.enabled` in config.yaml, and calls `reload_config()`.
Mirrors the existing DELETE pattern.

Also wire the previously-defined-but-unrouted `_handle_mcp_server_delete` into
`handle_delete`, and `_handle_mcp_server_update` into a new `handle_put` +
`do_PUT` in server.py — fixing a pre-existing bug where those handlers existed
but were never reachable over HTTP.

UI: add a toggle button in each MCP server row in the system settings panel
(panels.js). Clicking it calls PATCH and reloads the list. Toggle button is
styled with `.mcp-toggle-enabled` / `.mcp-toggle-disabled` CSS classes. The
`toggle_supported` flag in the list response is now `True`.

i18n: add 5 new keys (`mcp_enable_server`, `mcp_disable_server`,
`mcp_enabled_toast`, `mcp_disabled_toast`, `mcp_toggle_failed`) to all 9
non-English locales (English values as placeholder translations).

Tests: add `TestMcpToggle` class with 7 tests covering disable, enable,
404-not-found, empty name, missing field, response payload, and URL-encoded name.
Update `test_empty_config` and visibility panel assertions to reflect
`toggle_supported: True` and the new toggle button in panels.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 17:13:32 +00:00
nesquena-hermes 5d0d2bd0bf fix(updates): apply path must follow check-side fall-through past the latest tag
Fixes #2846. After PR #2758 (the #2653 fix) the update check correctly
falls through to the branch comparison when HEAD has moved past the
latest `v*` tag — so the banner reports the real commit count against
`origin/<branch>`. But `_select_apply_compare_ref` was never updated to
mirror that decision: as long as any `v*` tag exists, it returns
`tags[0]`, even when HEAD is far past it.

Result for everyone running hermes-agent past `v2026.5.16` (i.e. anyone
on agent master between tagged releases):

1. Banner: `Agent (origin/main): 254 updates available` ← correct
2. User clicks Update Now
3. `_select_apply_compare_ref` picks `v2026.5.16` because tags exist
4. `git pull --ff-only origin v2026.5.16` — no-op (HEAD is already past it)
5. `_schedule_restart()` fires anyway, server bounces
6. Next check still reports 254 behind — banner reappears unchanged

`apply_force_update` had the same bug, except worse: `git reset --hard
v2026.5.16` would have actively rewound the user's checkout 254 commits.

The root cause is the same bug class as #2653 — two parallel paths
(`_check_repo_release` and `_select_apply_compare_ref`) that should make
the same decision but didn't. Pre-fix, the "is HEAD past the latest
tag?" predicate lived inline inside `_check_repo_release` only.

Fix
---

Extract `_head_is_past_latest_tag(path, current_tag)` and have both
paths consult it. When HEAD is past the latest tag:

- check path:  release check returns None → branch check runs (#2653,
  unchanged behaviour, just refactored)
- apply path:  falls through to upstream / `origin/<branch>`, never the
  stale tag (#2846, new behaviour)

Tests
-----

- `test_select_apply_compare_ref_uses_tag_when_head_is_on_tag` —
  unchanged behaviour pinned: HEAD exactly on tag → advance to tag.
- `test_select_apply_compare_ref_falls_through_when_head_is_past_tag` —
  the #2846 repro: HEAD = v2026.5.16 + 608 commits → advance to
  `origin/main`, not the tag.
- `test_select_apply_compare_ref_no_tags_uses_upstream` — unchanged.
- `test_select_apply_compare_ref_no_tags_no_upstream_uses_default_branch`
  — unchanged.
- `test_check_and_apply_paths_agree_when_head_is_past_tag` — symmetry
  test, ensures the two paths can't drift apart again.

All 21 tests in `tests/test_updates.py` pass locally (16 existing + 5
new).

Refs #2846, #2653.
2026-05-24 17:13:32 +00:00
nesquena-hermes 71ba863ce5 fix(terminal): drop PR_SET_PDEATHSIG preexec_fn that killed every Linux shell
Fixes #2853. The `_terminal_shell_preexec_fn` added in `71d8a8fb` called
`prctl(PR_SET_PDEATHSIG, SIGTERM)` so orphaned PTY shells would die when
the WebUI process crashed. But that signal is **per-thread**, not
per-process, and WebUI runs `ThreadingHTTPServer`: every HTTP request is
handled in its own short-lived worker thread.

Flow that broke every Linux user:

1. User clicks the terminal toggle → frontend hits `POST /api/terminal/start`.
2. ThreadingHTTPServer spins up a worker thread to handle that one request.
3. The worker thread calls `subprocess.Popen(..., preexec_fn=...)`.
4. The shell calls `prctl(PR_SET_PDEATHSIG, SIGTERM)` in its preexec_fn.
   Its registered "parent" is now the WebUI worker thread that called Popen.
5. The handler returns its JSON response and the worker thread exits.
6. The kernel sees the pdeathsig-parent thread has died and sends SIGTERM
   to the PTY shell. The shell dies within ~10 ms of being created.
7. The reader loop sees EIO on the master FD, emits `terminal_closed`, and
   the frontend writes `[terminal closed]`.

macOS users were unaffected because `libc.prctl` doesn't exist there —
`ctypes.CDLL(None)` returns a libc handle, `libc.prctl` raises
`AttributeError`, the bare-`except` swallows it, and the shell starts
with no pdeathsig configured.

Empirical verification on this Linux host (real PTY + `subprocess.Popen`
inside a `threading.Thread` that joins immediately):

  with    preexec_fn → proc.poll() == -15 (SIGTERM), master FD returns EIO
  without preexec_fn → proc.poll() == None (alive), master FD returns "HELLO\\r\\n"

Same shell, same PTY, same threading topology as WebUI.

Fix
---

Drop the `preexec_fn` entirely. The orphan-shell-on-crash case the original
PR was navigating is rare for self-hosted single-user installs, and the
existing `atexit.register(close_all_terminals)` + explicit `close_terminal`
paths cover graceful shutdown. A future fix (option B in the issue) can
re-introduce pdeathsig pinned to a long-lived supervisor thread, but that
is a follow-up — this PR is the smallest unbricks-Linux-today change.

Tests
-----

- Invert `test_terminal_shell_uses_parent_death_signal_preexec` →
  `test_terminal_shell_does_not_use_pdeathsig_preexec`: asserts
  `preexec_fn` is NOT in the Popen kwargs.
- Add `test_pty_shell_survives_when_spawning_thread_exits`: spawns a
  real PTY shell via `start_terminal` from a worker thread, waits for
  the worker to join, asserts the shell is still alive after a half-second
  grace window. This is the contract the original tests never exercised.
- Update `test_terminal_module_registers_graceful_shutdown_reaper` to
  refuse re-introduction of the preexec_fn or the `libc.prctl(1, SIGTERM)`
  call (treats either as a regression).

All 27 terminal-related tests pass locally.

Refs #2853
2026-05-24 17:13:31 +00:00
Frank Song 99c886c199 fix(workspace): open rendered preview links correctly 2026-05-24 15:52:35 +00:00
Frank Song 67a204773e fix(csrf): clarify rejection diagnostics 2026-05-24 15:52:34 +00:00
AJV20 b6f7412b53 Add option to ignore agent updates 2026-05-24 15:52:34 +00:00
Abdul Munim 7999d1c75a feat(workspace): add Open in VS Code action for files and folders (#2735)
Right-click any workspace file, folder, or root now shows
'Open in VS Code' alongside the existing Reveal in File Manager action.

- POST /api/file/open-vscode: resolves path via safe_resolve, finds VS
  Code via shutil.which() with fallbacks for macOS (/usr/local/bin/code,
  app bundle CLI), Linux (/usr/bin/code, /snap/bin/code), and Windows
  (%LOCALAPPDATA% and %PROGRAMFILES% user/system installs). Returns a
  descriptive error if not found rather than a bare OS error.
- Optional vscode block in config.yaml: command (default: code),
  host_path_prefix + container_path_prefix for Docker path mapping.
- i18n: open_in_vscode and open_in_vscode_failed translated in all 10
  locales (it, ja, ru, es, de, zh-CN, zh-TW, pt, ko).
- 26 tests in tests/test_2735_open_in_vscode.py covering source wiring,
  command resolution, i18n completeness, and live endpoint error paths.
2026-05-24 04:26:46 +00:00
Qi d20da832b3 fix(static): tighten cache validators and 304 headers 2026-05-24 04:26:46 +00:00
ai-ag2026 225ea78604 fix: drop stale cached user tail after saved assistant 2026-05-24 04:06:45 +00:00
Simonas Jakubonis 35c55e1268 fix(compression): ignore tool output for compaction cards 2026-05-24 03:55:42 +00:00
ai-ag2026 cd029d801a fix: align messaging session display counts 2026-05-24 03:55:42 +00:00
carryzuo00 ee672df463 fix: prevent state.db messages being silently dropped during sidecar merge
Two bugs combined to cause historical messages to vanish from the WebUI
after a session was continued in a later conversation.

**Bug 1 — missing `id` in state.db SELECT (models.py)**
`get_state_db_session_messages()` did not include the `id` column in its
SELECT, so every row got a `("legacy", ...)` merge key instead of
`("message_id", ...)`.  The timestamp gate in
`merge_session_messages_append_only()` explicitly exempts `message_id`-keyed
rows from its "skip if older than newest sidecar message" rule, but
legacy-keyed rows are unconditionally dropped.  With a session that has any
new sidecar messages (max_sidecar_timestamp == today), all older state.db
rows were silently discarded.

Fix: include `id` when the column is present so rows get proper
`("message_id", ...)` keys and survive the timestamp filter.

**Bug 2 — always reads active profile's state.db, not the session's (models.py + routes.py)**
`get_state_db_session_messages()` always called `_active_state_db_path()`,
which returns the currently-active profile's database.  Sessions belonging to
a different profile (e.g. `jump`) were read from the wrong state.db, returning
either no rows or unrelated ones.

Fix: add an optional `profile` parameter; when supplied, resolve the path via
`_get_profile_home(profile)` with a fallback to the active path if the
profile-specific db does not exist.  The call-site in `routes.py` now reads
`session.profile` and passes it through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 03:55:42 +00:00