Commit Graph

3500 Commits

Author SHA1 Message Date
nesquena-hermes 4fa051ea14 fix(security/#3234 + #3219): unify case-folded path checks, fix workspace over-block, protect raw <pre> (Codex review #6)
1. routes.py: hoist a single case-folded path helper (_norm/_within_ci/_equal_ci)
   used for ALL deny + carve-out comparisons (consistent macOS/Windows safety).
2. routes.py: split the deny into (a) dir-based denies that ALWAYS fire (even
   inside the active workspace — so a workspace overlapping a state dir cannot
   expose sessions/memories), and (b) filename denies relaxed only by the
   carve-out. Fix the over-block: a workspace that is a proper DESCENDANT of a
   Hermes root (e.g. STATE_DIR/workspace) is a legit project workspace and keeps
   the carve-out; only a root-itself / ancestor / $HOME / profiles / state-subdir
   workspace disables it.
3. ui.js: move the bare file:// media-stash pass after the raw-<pre> stash too,
   so file:// inside a raw <pre> block stays literal (not just fenced/inline code).
2026-05-31 03:24:35 +00:00
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 68a6099eaf fix(security #3234): harden /api/media deny-list (Codex review)
- Add state.db-wal / state.db-shm (SQLite sidecars carry the same data as state.db)
- Add google_token.json / google_client_secret.json (OAuth creds)
- Scope filename-based denies to files under HERMES_HOME / STATE_DIR so a
  legitimate workspace or /tmp media artifact named settings.json / config.yaml
  is not wrongly blocked.
Dir-based denies (state subdirs) remain unconditional.
2026-05-31 02:39:46 +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 0405fefb5e Merge pull request #3233 from nesquena/release/stage-batchB2
Release stage-batchB2 → v0.51.182 (consolidated client-disconnect handling #3210)
v0.51.182
2026-05-30 19:06:43 -07: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
nesquena-hermes c4d845c9d5 docs(CHANGELOG): v0.51.182 — consolidated client-disconnect handling (#3210) 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 78652ecdea fix: consolidate _CLIENT_DISCONNECT_ERRORS, remove scratch file
Address review feedback from @nesquena-hermes on PR #3210:

1. Deduplicate _CLIENT_DISCONNECT_ERRORS:
   - Single authoritative definition in api/helpers.py
   - api/routes.py now imports from api.helpers instead of defining
     its own copy with different membership
   - Unified tuple uses OSError (covers ssl.SSLError since it
     subclasses OSError) — broad socket-level disconnect coverage

2. Remove github-search-report.md:
   - Research scratch output that doesn't belong in the repo root
   - Content belongs in PR description or a gist

3. Docstring improvement:
   - Added comment explaining why OSError covers ssl.SSLError
   - Documents the errno-level socket errors caught by OSError
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
someaka 2cee44b1b2 fix(helpers): catch client disconnect errors in response write path
Extract _safe_write() helper that wraps end_headers() + wfile.write()
in try/except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError,
TimeoutError, ssl.SSLError).  Both j() and t() now use _safe_write()
instead of raw wfile calls.

Fixes cascading BrokenPipeError + SSL BAD_LENGTH crash when a client
disconnects mid-response and the error handler tries to write a 500
status through the same broken socket.
2026-05-31 01:56:39 +00:00
Ed 5915d675a0 docs: add GitHub search report — upstream issues/PRs related to fork commits 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 82fe933b5a Merge pull request #3221 from nesquena/release/stage-batchA
Release stage-batchA → v0.51.181 (agent-cache eviction teardown #3218 + streaming finalize race #3201)
v0.51.181
2026-05-30 17:31:34 -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
nesquena-hermes 7607f3f100 Merge #3218: route identity-mismatch agent-cache evictions through session-boundary teardown (closes #3215) 2026-05-31 00:06:13 +00:00
AJV20 6ecc32be00 Close evicted cached agents on identity mismatch 2026-05-30 19:25:51 -04:00
nesquena-hermes 82f92eb78d Merge pull request #3216 from nesquena/release/stage-batch62
Release stage-batch62 → v0.51.180 (session/agent cache ownership hardening — #3191 + #3166)
v0.51.180
2026-05-30 15:21:06 -07: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 f50aac68b2 Merge pull request #3213 from nesquena/release/stage-batch61
Release stage-batch61 → v0.51.179 (custom-provider reasoning efforts #3202 + clearer sidebar tooltips #3203)
v0.51.179
2026-05-30 14:54:15 -07: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 e41fee1dfd Merge pull request #3212 from nesquena/release/stage-batch60
Release stage-batch60 → v0.51.178 (parallel sharded CI + shard-safe test suite, completes #3197)
v0.51.178
2026-05-30 14:12:18 -07: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 113c9791f8 Merge pull request #3207 from nesquena/release/stage-batch59
Release stage-batch59 → v0.51.177 (Docker smoke-test layer caching, #3197 docker half)
v0.51.177
2026-05-30 13:14:31 -07:00
nesquena-hermes f4772ff081 ci(docker): cache smoke-test image layers via GHA cache (#3197 docker half)
Build the Docker image once in a build-image job, cache layers via type=gha,
restore from cache in each smoke variant instead of rebuilding. Variant restore
uses cache-from only (build-image is the sole cache writer).

Co-authored-by: hayriodabas <hayriodabas@users.noreply.github.com>
2026-05-30 20:10:27 +00:00
nesquena-hermes f8f1052d53 Merge pull request #3206 from nesquena/release/stage-batch58
Release stage-batch58 → v0.51.176 (sidebar attention indicators)
v0.51.176
2026-05-30 13:07:44 -07: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
nesquena-hermes 0560a3fa8b Merge pull request #3205 from nesquena/release/stage-batch57
Release stage-batch57 → v0.51.175 (internal conversation links)
v0.51.175
2026-05-30 12:23:12 -07:00
ai-ag2026 d2aa066862 feat: surface session attention badges 2026-05-30 19:18:13 +00:00
nesquena-hermes 9e38b0ff90 docs(changelog): v0.51.175 release header (stage-batch57) 2026-05-30 19:16:12 +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
nesquena-hermes 05122e263d Merge pull request #3204 from nesquena/release/stage-batch56
Release stage-batch56 → v0.51.174 (CLI/gateway session usage in Insights, double-count fix)
v0.51.174
2026-05-30 12:11:37 -07:00
nesquena-hermes 7ec16ba30c docs(changelog): v0.51.174 release header (stage-batch56) 2026-05-30 19:08:08 +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