Commit graph

1955 commits

Author SHA1 Message Date
nesquena-hermes
5758d4a827 fix(security/#3234 + #3219): case-fold deny checks + make bare file:// rewrite code-fence-aware (Codex review #5)
1. api/routes.py: case-fold /api/media deny filename + dir containment checks
   (os.path.normcase + casefold) so STATE.DB / Sessions/ cannot bypass the
   state/secret deny on case-insensitive filesystems (macOS/Windows).
2. static/ui.js: move the bare file:// media-stash pass to run AFTER fenced-block
   and inline-code stashing, so a file:// inside a code block / backtick span
   stays literal text instead of becoming an auto-loaded <img>. The MEDIA: stash
   keeps its first-position precedent.
Adds behavioral renderer tests (real renderMd via node) for fenced + inline code
file:// staying literal, bare file:// becoming media, and anchors keeping the
link path. Closes the last Codex review items for #3234.
2026-05-31 03:17:10 +00:00
nesquena-hermes
26305c2ebb fix(security #3234): gate active-workspace carve-out against broad/internal roots (Codex review #4)
Codex round-4: the carve-out could re-open the hole if the active workspace is
pathologically set to a broad/internal root ($HOME, ~/.hermes, a profile root)
— get_last_workspace only checks is_dir(), so workspace=~/.hermes would serve
state.db. Gate the carve-out: disable it when the active workspace IS, CONTAINS,
or is CONTAINED BY any Hermes root, or is $HOME / a */profiles dir / a named
profile root / an internal state subdir. Adds a unit test proving state.db stays
403 when the active workspace is the Hermes home. Widen CSP-slice test window.
2026-05-31 03:08:10 +00:00
nesquena-hermes
343bad7263 fix(security #3234): redesign /api/media deny with active-workspace carve-out (Codex review #3)
Codex round-3 found the prior multi-profile hardening OVER-blocked: denying
STATE_DIR + base/profiles wholesale 403'd legitimate active-workspace media.
Redesign around a single principle: the ACTIVE WORKSPACE is the user's own
content (never deny), Hermes INTERNAL STATE lives outside any workspace (deny).
If target is inside the active workspace -> allow; else deny known secret/config
basenames + internal state subdirs across all Hermes roots. Also folds in Opus
defense-in-depth: adds cron/logs/checkpoints/backups subdirs +
gateway_state.json/channel_directory.json/jobs.json basenames. Adds an
over-block regression test (a /tmp artifact named settings_* still serves 200).
2026-05-31 02:55:53 +00:00
nesquena-hermes
c514b32f61 fix(security #3234): cover ALL Hermes roots in /api/media deny-list (Codex review #2)
Under a named profile, process HERMES_HOME is ~/.hermes/profiles/<name> but the
allowlist still grants base ~/.hermes — so the prior deny (anchored only on the
active-profile root + STATE_DIR) left ~/.hermes/state.db and sibling-profile
secrets (~/.hermes/profiles/other/auth.json) reachable. Build deny roots from
every Hermes state root the allowlist accepts: active HERMES_HOME, base ~/.hermes,
api.profiles._DEFAULT_HERMES_HOME, and STATE_DIR; apply the state-subdir dir-denies
under each. Widen the CSP-slice structural test window to match.
2026-05-31 02:45:17 +00:00
nesquena-hermes
c36fda23f7 test: widen _handle_media CSP-slice window to 7000 after #3234 deny-list insertion 2026-05-31 02:32:16 +00:00
nesquena-hermes
63bb2f9884 fix(security #3234): /api/media hard-denies WebUI state + secret/config files
Pre-release dual-gate (Codex + Opus) on #3219 surfaced that /api/media serves
files under the allowlisted Hermes home, including settings.json / state.db /
auth.json / config.yaml. #3219 makes this materially worse: pre-#3219 a bare
file:// URL in agent output rendered as inert text, but #3219 turns it into an
auto-loading <img src=/api/media?path=...> that fetches on render. Rather than
weaken #3219, harden the boundary at the route: hard-deny known secret/config
filenames and the WebUI state subdirs (sessions/memories/profiles + STATE_DIR)
before the allow/serve decision, covering every entry path (bare file://,
markdown anchors, MEDIA: tokens, session-token grants). Adds a live-server
regression test. Closes #3234.

Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
2026-05-31 02:27:15 +00:00
AJV20
aae3b418f0 fix: render file artifact links as media 2026-05-31 02:24:55 +00:00
nesquena-hermes
dca4a2a7af fix(#3210 review): convert bare urlopen TimeoutError to ValueError in _joplin_api_get
Codex+Opus pre-release gate both flagged: TimeoutError is now in the
consolidated _CLIENT_DISCONNECT_ERRORS dispatch set, so a bare socket-connect
TimeoutError from Joplins urlopen(timeout=8) — which is NOT always URLError-
wrapped — would escape _handle_notes_search and be swallowed by the dispatch
disconnect handler as a fake client disconnect (silent empty response, no log).
Catch (URLError, TimeoutError) at the route so it surfaces as a clean
"not reachable" ValueError -> JSON error. Adds a regression test.

Co-authored-by: someaka <someaka@users.noreply.github.com>
2026-05-31 01:57:29 +00:00
Ed
79f526b1b2 fix(helpers): use ssl.SSLError instead of broad OSError in disconnect tuple
OSError is too broad — it masks real errors like file-not-found.
ssl.SSLError specifically catches SSL-level disconnects without
swallowing unrelated OSError subtypes.

Closes the test_excludes_broad_oserror CI failure.
2026-05-31 01:56:39 +00:00
Ed
59b7a3ac94 fix: eliminate silent failures in client disconnect handling
- api/helpers.py: _safe_write() now logs disconnects at debug level
  instead of silently passing. No more invisible errors.

- server.py: Restructure exception handlers to catch
  _CLIENT_DISCONNECT_ERRORS first, then Exception. Remove the
  isinstance() filter inside except Exception (LBYL anti-pattern).
  The 500-response fallback now catches _CLIENT_DISCONNECT_ERRORS
  separately (expected) and logs unexpected failures via
  traceback.print_exc() instead of bare except Exception: pass.

- tests/test_broken_pipe_cascade.py: Add coverage for SSL/Timeout
  disconnect routing and 500-response safety (both disconnect
  survival and unexpected error logging).
2026-05-31 01:56:39 +00:00
someaka
e531a05e60 fix(server): catch client disconnect errors in response write path 2026-05-31 01:56:39 +00:00
nesquena-hermes
5b2cbe1323
ci: add headless browser smoke test (runtime brick-class gate) (#3231)
Boots the real server.py agent-free and loads the key pages in headless
Chromium, failing on any console error or uncaught JS exception. Catches the
runtime-JS brick class (const-reassign #3162, function/window collision
#2715/#2771) that node --check, ESLint, and the mocked pytest suite cannot see
because they only manifest when a real browser executes the page.

Credential-free: strips *_API_KEY from the env, drives no real model, needs no
secrets. Runs on every PR + push via .github/workflows/browser-smoke.yml.

Co-authored-by: nesquena-hermes <[email protected]>
2026-05-30 18:48:25 -07:00
nesquena-hermes
634a70bebf test(#3218): extend cache-eviction lock-discipline guard to the self-heal site (Opus NIT)
Opus advisor noted the static guard covered 4 of the 5 identity-mismatch
eviction sites; add the credential-self-heal pop/close pair so a future
re-lock of that path is caught by the gate too.
2026-05-31 00:26:59 +00:00
nesquena-hermes
5134d896ba absorb #3201: set _streamFinalized immediately in SSE done handler (closes #3195)
Keeps the messages.js fix from @mysoul12138 (set _streamFinalized=true right
after the early-return guard, before the fade window, so a stream_end arriving
mid-fade can't call _restoreSettledSession and overwrite live messages with a
stale server snapshot). Drops the contributor's dead empty `else {}` block in
ui.js (no behavior). Adds a regression test pinning the immediate-finalize order.

Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
2026-05-31 00:07:39 +00:00
AJV20
6ecc32be00 Close evicted cached agents on identity mismatch 2026-05-30 19:25:51 -04:00
nesquena-hermes
22e628be07 Merge #3191: guard session cache ownership across compression
# Conflicts:
#	CHANGELOG.md
2026-05-30 21:57:48 +00:00
nesquena-hermes
bc50e95ec7 Merge #3166: close evicted agents from WebUI cache (commit memory + shutdown provider + close db) 2026-05-30 21:57:13 +00:00
nesquena-hermes
d6d74b5354 polish(#3203): preserve localized 'Forked from' base in fork tooltip
Opus advisor SHOULD-FIX: the tooltip rework dropped t('forked_from') in favor
of a hardcoded English string, a real i18n regression (the catalog key exists
in all locales). Restore the localized base while keeping the clearer
'<base>: <parent>' format. Adds a regression test.

The new lineage/child explanatory suffixes (additive English) and the
read-only title hint are deferred to a small follow-up — they need new locale
keys and the read-only state is still surfaced by the existing meta chip.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-30 21:51:17 +00:00
nesquena-hermes
cceeca89d0 polish(#3203): keep localized attention tooltip precedence over generic state tooltip
The merged hunk set state.title=_sessionStateTooltip(...) unconditionally,
two lines after assigning the localized attention.title (pending approval/
clarify, from #3190). That clobbered the attention tooltip and, for a
needs-attention session not currently streaming, blanked it to '' since
_sessionStateTooltip returns '' when neither streaming nor unread.

Make the attention title win, and apply the state tooltip only when non-empty.
Adds a regression test pinning the precedence.

Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-05-30 21:46:01 +00:00
nesquena-hermes
5cd2aef0d5 Merge #3203: clarify sidebar session tooltips
# Conflicts:
#	CHANGELOG.md
2026-05-30 21:43:03 +00:00
nesquena-hermes
2a60aa048c Merge #3202: reasoning effort for bare/dot-separated custom-provider model names 2026-05-30 21:42:26 +00:00
nesquena-hermes
f9acf464e9 ci: run test suite in 3 parallel shards + make suite shard-safe
Completes the test-sharding half of #3197 (Docker-cache half shipped v0.51.177).
Adds pytest-shard 3-way split to tests.yml (3 shards x 3 Python = 9 jobs,
fail-fast: false). pytest-shard is 0-indexed so the matrix uses [0,1,2] — the
original #3197 used [1,2,3] which would have crashed the out-of-range job and
silently skipped shard 0's tests.

Made the suite shard-safe by fixing 4 cross-test state-pollution bugs that
passed sequentially but failed when sharded:
- test_onboarding_mvp: reset onboarding_completed flag (settings.json) in the
  autouse fixture; the config-cleanup only cleared config.yaml/.env.
- test_issue693_system_health_panel: invalidate the process-wide password-hash
  cache before/after so a prior test's "no password" cache doesn't defeat the
  auth-gate assertion.
- test_auth_session_persistence: assert against auth._SESSIONS_FILE (where auth
  actually writes) instead of a local _TEST_STATE path that only matched under a
  lucky import order.
- test_profile_env_isolation (root cause of the worst leak): stop deleting +
  re-importing api.profiles under a temp HERMES_BASE_HOME — that swapped the
  module object and poisoned the cached _DEFAULT_HERMES_HOME for every later
  test (broke test_title_aux_routing's load_config). Now points the cached path
  via monkeypatch.setattr (auto-restored, no module swap).
- conftest: autouse fixture restores HERMES_HOME/HERMES_BASE_HOME after each
  test as defense-in-depth against future switch_profile leaks.

Verified: all 3 shards green (6912 passed, 0 failed); full sequential run still
green (6957 passed, 0 failed). Slowest shard ~70s vs ~180s sequential.
2026-05-30 21:09:25 +00:00
nesquena-hermes
4ef544cfa4 Merge branch 'pr-3190' into release/stage-batch58
# Conflicts:
#	CHANGELOG.md
2026-05-30 19:56:01 +00:00
Hermes Agent
01c1239fc9 refactor(sessions): simplify attention indicator to a color-coded dot
Per maintainer UX direction: drop the text badge pill ("APPROVAL" /
"N QUESTIONS") in favor of color-coding the existing right-side status dot
plus the colored left rail. Red dot (--error) for pending approvals, amber
dot (--warning) for pending clarifies; theme-driven, visible even when the
session is not streaming/unread. Approval dot gently pulses (disabled under
prefers-reduced-motion). Quieter and consistent with the existing pin/unread
dot family — the row no longer needs to truncate its title to fit a pill.

Updated test_session_attention_badges to assert the dot classes
(is-attention-approval/clarify) instead of the removed badge element/styles.

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-30 19:54:51 +00:00
ai-ag2026
00e6d953c2 feat: add distinct attention request sound
Adds a distinct two-tone attention sound (880->660Hz) for approval and
clarify prompts so they are not confused with the existing completion sound,
plus sidebar attention badges + colored rails driven by `attention` metadata
on /api/sessions. Includes lock-safety note: the in-lock
publish_session_list_changed() calls in clarify.py are safe because publish()
only takes the leaf _SESSION_EVENTS_LOCK and never re-acquires clarify._lock
(verified by Opus advisor review).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-30 19:40:40 +00:00
ai-ag2026
d2aa066862 feat: surface session attention badges 2026-05-30 19:18:13 +00:00
ai-ag2026
8e44dc8609 feat: resolve session links in conversation filter 2026-05-30 19:13:49 +00:00
ai-ag2026
ce4026660e test: harden session link sanitization 2026-05-30 19:13:11 +00:00
ai-ag2026
d3ee17793a feat: add internal conversation links 2026-05-30 19:13:11 +00:00
Hermes Agent
06842f6e9f fix(insights): exclude source='webui' from CLI session pass + add tests
The new CLI/gateway insights pass counted ALL state.db sessions, but
WebUI-native sessions are persisted to state.db with source='webui' AND
already counted from the sidecar _index.json first pass — double-counting
them in totals, model breakdown, and daily charts. Add
`AND COALESCE(source, '') != 'webui'` so only CLI/gateway/cron/tui rows are
added by the second pass.

Adds regression tests proving (a) CLI + Telegram sessions appear in totals
and (b) a webui-source state.db row is not double-counted against its
_index.json entry.

Co-authored-by: wind-chant <wind-chant@users.noreply.github.com>
2026-05-30 18:58:20 +00:00
ai-ag2026
c26e25414e fix: clarify sidebar session tooltips 2026-05-30 20:56:59 +02:00
carryzuo00
d6c1b696f5 fix(reasoning): expose effort levels for bare/dot-separated model names on custom providers
Custom API aggregators (New API, One API, etc.) route requests using their
own naming conventions — bare names like ``deepseek-v4-flash`` or dot-separated
names like ``moonshotai.kimi-k2.5`` — rather than the OpenRouter-style
``vendor/model`` slash format that ``_heuristic_reasoning_efforts``'s prefix
list was written for.

Because none of these names match the slash-prefixed patterns, the function
returned ``[]``, hiding the reasoning effort selector in the WebUI even for
models that fully support thinking/reasoning.

Fix: add a secondary check in ``_heuristic_reasoning_efforts`` that strips an
optional dot-vendor prefix and matches the remaining model name against a list
of known thinking-capable bare-name prefixes (``deepseek-v4``, ``deepseek-r1``,
``kimi-k2``, ``qwen3``, ``claude-3/4``, ``o1/o3/o4``), plus a keyword catch
for model names containing ``thinking`` or ``reasoning``.

The slash-prefix path and all provider-specific fast paths (copilot, lmstudio,
models.dev metadata) are unchanged; this block only runs as a final fallback.

Regression tests added in ``tests/test_custom_provider_bare_model_reasoning.py``.
2026-05-31 02:51:26 +08:00
nesquena-hermes
025dd738b9 Merge branch 'pr-3178' into release/stage-batch55
# Conflicts:
#	CHANGELOG.md
2026-05-30 18:25:55 +00:00
nesquena-hermes
91f33a50cd Merge branch 'pr-3192' into release/stage-batch55
# Conflicts:
#	CHANGELOG.md
2026-05-30 18:24:30 +00:00
nesquena-hermes
db952ac561 Merge branch 'pr-3187' into release/stage-batch55
# Conflicts:
#	CHANGELOG.md
2026-05-30 18:24:29 +00:00
nesquena-hermes
58aec3e3f8 Merge branch 'pr-3184' into release/stage-batch55
# Conflicts:
#	CHANGELOG.md
2026-05-30 18:24:29 +00:00
nesquena-hermes
6d1ae11522 Merge branch 'pr-3180' into release/stage-batch55
# Conflicts:
#	CHANGELOG.md
2026-05-30 18:24:28 +00:00
nesquena-hermes
9e48a2a417 Merge branch 'pr-3183' into release/stage-batch54
# Conflicts:
#	CHANGELOG.md
2026-05-30 17:52:53 +00:00
nesquena-hermes
637ae65383 Merge branch 'pr-3182' into release/stage-batch54 2026-05-30 17:52:53 +00:00
ai-ag2026
9458df6f4d fix: dedupe sidebar completion session ids 2026-05-30 18:19:34 +02:00
ai-ag2026
9932899b88 fix: guard session cache ownership across compression 2026-05-30 18:19:31 +02:00
mysoul12138
71e6eabb29 fix: let cron sessions bypass CLI_VISIBLE_SESSION_LIMIT for project chips (#3172)
When state.db has many non-cron sessions, the normal sidebar query caps
at CLI_VISIBLE_SESSION_LIMIT (20) rows ordered by latest activity. Older
cron runs get squeezed out before _include_project_hidden_background_sidebar_sessions
can rescue them, making them invisible under their project chip.

Add a second-pass cron-only query with a higher cap (CRON_PROJECT_CHIP_LIMIT=200)
that merges into the CLI session list.  The project-chip rescue layer then
marks them default_hidden so they stay addressable without polluting the
default sidebar window.

Verification: regression test seeds 25+ newer non-cron sessions and asserts
the older messageful cron session still appears with project_id set.
2026-05-30 23:25:41 +08:00
ai-ag2026
4639d355ee test: cover hidden snapshot pin quota route 2026-05-30 16:31:24 +02:00
ai-ag2026
b891e4fe77 fix: avoid double rebasing legacy tool cards 2026-05-30 16:10:52 +02:00
ai-ag2026
d57854807a fix: keep tool cards anchored during history paging 2026-05-30 14:57:00 +02:00
ai-ag2026
09d2f4be2f fix: exclude hidden snapshots from pin quota 2026-05-30 11:48:57 +02:00
ai-ag2026
cb01e041fd fix: preserve tilde workspace suggestions 2026-05-30 11:35:05 +02:00
ai-ag2026
000e1dc9b6 fix: hash dirty WebUI dev builds 2026-05-30 11:25:33 +02:00
ai-ag2026
fb7fa5bcac fix: handle Windows media and turn journal edge cases 2026-05-30 11:15:35 +02:00
ai-ag2026
b3483d943c fix: close evicted agents from WebUI cache 2026-05-30 10:43:13 +02:00