Commit Graph

1905 Commits

Author SHA1 Message Date
nesquena-hermes 96ca83bf53 fix(security): drop unsafe-eval + add jsdelivr to CSP, sanitize plugin error
Opus stage-339 review SHOULD-FIX items:

1. server.py: drop 'unsafe-eval' from CSP report-only policy.
   Verified by grepping all production JS — zero matches for eval(),
   new Function(), or string-form setTimeout/setInterval. Keeping it
   was a gratuitous privilege.

2. server.py: add https://cdn.jsdelivr.net to script-src + style-src.
   index.html loads Prism/xterm/katex from this CDN with SRI hashes —
   without the allowance every page load fires known-good CSP violations
   that drown out real signal once a collector is wired.

3. api/commands.py: sanitize plugin command error. Previously returned
   f'Plugin command error: {exc}' which would leak paths/env from
   FileNotFoundError('/etc/something/secret.key') etc. Now returns only
   the exception type name; full traceback goes to server log.

Test asserts updated to match the new policy shape.

Co-authored-by: Opus advisor <opus-advisor@hermes.local>
2026-05-11 17:53:02 +00:00
nesquena-hermes 55fbe66c79 docs: CHANGELOG Unreleased — stage-339 (5-PR batch + turn-journal stack) 2026-05-11 17:44:34 +00:00
nesquena-hermes fd069155af Merge PR #2062 into stage-339
feat: record turn journal lifecycle events
by @ai-ag2026
2026-05-11 17:43:58 +00:00
nesquena-hermes f6ce79185c Merge PR #2059 into stage-339
feat: add crash-safe turn journal writer
by @ai-ag2026
2026-05-11 17:43:58 +00:00
nesquena-hermes 2a1244f342 Merge PR #2089 into stage-339
support slash commands implemented in hermes plugin
by @plerohellec
2026-05-11 17:43:57 +00:00
nesquena-hermes 0456fb5619 Merge PR #2085 into stage-339
fix(logs): clipboard fallback + severity filter for Logs panel (#2081)
by @bergeouss
2026-05-11 17:43:56 +00:00
nesquena-hermes 9db1da76bd Merge PR #2084 into stage-339
fix: add report-only CSP header
by @ai-ag2026
2026-05-11 17:43:55 +00:00
nesquena-hermes 6b170513d4 Merge pull request #2091 from nesquena/stage-338
Release U — v0.51.45 (9-PR contributor batch — themes docs + skill cache + lineage forks + spinner + slug + recovery polish + compression anchor)
v0.51.45
2026-05-11 10:33:46 -07:00
nesquena-hermes 85ae0acbdc docs: CHANGELOG v0.51.45 Release U (9-PR batch + Opus SHOULD-FIX) 2026-05-11 17:31:18 +00:00
nesquena-hermes 83de9d0cf0 fix(providers): log warning when custom provider entry yields empty slug
Opus stage-338 review SHOULD-FIX: silent drop at api/providers.py:1049
was diagnostically opaque. logger.warning() now surfaces the bad
config entry so operators can spot misconfigurations.

Co-authored-by: Opus advisor <opus-advisor@hermes.local>
2026-05-11 17:30:56 +00:00
nesquena-hermes 87bd9ea372 docs: CHANGELOG Unreleased — stage-338 (9 PRs) 2026-05-11 17:18:16 +00:00
nesquena-hermes 6a016dae6c Merge PR #2077 into stage-338
Refactor compression anchor visibility helpers
by @franksong2702
2026-05-11 17:17:25 +00:00
nesquena-hermes 98b6925333 Merge PR #2065 into stage-338
Fix session recovery polish
by @franksong2702

# Conflicts:
#	CHANGELOG.md
2026-05-11 17:17:24 +00:00
nesquena-hermes 0662f0986f Merge PR #2056 into stage-338
Fix custom provider name slugs with ports
by @franksong2702

# Conflicts:
#	CHANGELOG.md
2026-05-11 17:17:19 +00:00
nesquena-hermes 4388cb1a10 Merge PR #2068 into stage-338
fix(ui): prevent stuck sidebar spinner on completed sessions (closes #2066)
by @franksong2702
2026-05-11 17:17:05 +00:00
nesquena-hermes 2bfd538714 Merge PR #2063 into stage-338
fix: keep explicit forks out of lineage report
by @dso2ng
2026-05-11 17:17:05 +00:00
nesquena-hermes ee6c67f30c Merge PR #2074 into stage-338
Fix HERMES_HOME skill cache patching
by @franksong2702
2026-05-11 17:17:04 +00:00
nesquena-hermes da6b897e54 Merge PR #2076 into stage-338
test: add kanban locale parity check (refs #1973)
by @bergeouss
2026-05-11 17:17:03 +00:00
nesquena-hermes d87b23e76f Merge PR #2073 into stage-338
test: allow top-level markdown docs
by @ai-ag2026
2026-05-11 17:17:01 +00:00
nesquena-hermes 7037b084de Merge PR #2088 into stage-338
docs(themes): align THEMES.md with Theme × Skin architecture
by @michael-dg
2026-05-11 17:17:00 +00:00
Philippe Le Rohellec 281a57b60a support slash commands implemented in hermes plugin 2026-05-11 09:42:40 -07:00
Michael De Gols 0f8ba4d8d3 docs(themes): align THEMES.md with Theme × Skin architecture
THEMES.md still described the pre-#627 model where each theme was a
monolithic palette name (Dark, Light, Slate, Solarized Dark, Monokai,
Nord, OLED). The current architecture splits appearance into two
orthogonal pickers:

- Theme (System / Dark / Light) — applied as `.dark` class on <html>
- Skin (8 named accent palettes) — applied as `data-skin` attribute

Rewrite the doc to:
- Open with the Theme × Skin separation and how they combine
- List the 3 themes and 8 actual skins shipped in static/style.css
  (default, ares, mono, slate, poseidon, sisyphus, charizard, sienna),
  with the same descriptive tone as the original
- Replace "Creating a Custom Theme" with "Creating a Custom Skin" as
  the primary extension point, with paired light + dark CSS variants
- Note the WebUI extensions surface (docs/EXTENSIONS.md) as a
  no-fork path for self-hosted custom skins
- Update internals to reflect classList.toggle('dark') + dataset.skin
  + dataset.fontSize instead of the old data-theme-only model
- Add a brief Font Size section since it sits in the same picker
- Keep a smaller Custom Theme section for the rare case someone wants
  to override the core palette, redirecting most users to skins

Docs-only change; no code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:35:12 +02:00
bergeouss 85547612fe fix(logs): clipboard fallback + severity filter for Logs panel (#2081)
- replace navigator.clipboard.writeText with _copyText (has textarea fallback)
- add severity filter dropdown (All / Errors / Warnings+)
- add _severityForLine and _filteredLogsLines helpers
- add logsSeverityFilter HTML element + CSS class hooks
- add 5 new i18n keys across all 8 locales
- update test_logs_ui_static.py to match new implementation

Closes #2081
2026-05-11 15:40:49 +00:00
ai-ag2026 80c12123d2 Merge branch 'master' into fix/csp-report-only 2026-05-11 17:26:45 +02:00
ai-ag2026 c3fea4db3e fix: add report-only CSP header 2026-05-11 17:26:20 +02:00
ai-ag2026 c864ad47af fix: address turn journal lifecycle review 2026-05-11 17:16:43 +02:00
ai-ag2026 d04d48f5a0 fix: harden turn journal submitted writes 2026-05-11 17:13:57 +02:00
ai-ag2026 c4b7a65356 test: keep local context docs ignored 2026-05-11 17:09:19 +02:00
Frank Song 6a52edf2ab Fix stale inflight purge runtime lookup 2026-05-11 21:53:43 +08:00
Frank Song 18124ced62 Refactor compression anchor visibility helpers 2026-05-11 20:56:30 +08:00
bergeouss c0ccefd322 test: add kanban locale parity check (refs #1973)
Add test_kanban_locale_parity to test_kanban_ui_static.py that asserts
every kanban_* i18n key in the English locale exists in all non-English
locale blocks. Pattern follows test_lineage_segment_locale_keys_are_defined_for_sidebar_locales.
2026-05-11 12:38:48 +00:00
Frank Song c8d110a7f0 test: align sidebar spinner state assertions 2026-05-11 20:31:00 +08:00
Frank Song a0e9c06102 Fix HERMES_HOME skill cache patching 2026-05-11 19:12:02 +08:00
ai-ag2026 d30263bcf1 test: allow top-level markdown docs 2026-05-11 12:36:35 +02:00
Frank Song c60078b356 fix(ui): prevent stuck sidebar spinner on completed sessions (closes #2066)
The spinner (.session-state-indicator.is-streaming) can remain spinning
indefinitely on completed sessions when the INFLIGHT in-memory cache is
not cleaned up due to abnormal stream termination (page refresh, network
disconnect, gateway restart).

Add a staleness guard in _isSessionLocallyStreaming: if the server
reports is_streaming=false and last_message_at is older than 5 minutes,
force the streaming state to false regardless of stale INFLIGHT entries.
2026-05-11 17:54:14 +08:00
Frank Song f6115b78c6 Fix custom provider name slugs with ports 2026-05-11 17:24:53 +08:00
Dennis Soong 5efd287264 fix: align fork lineage projection paths 2026-05-11 17:15:22 +08:00
Frank Song 2cd10868aa Fix session recovery polish 2026-05-11 16:30:25 +08:00
Dennis Soong 1e8d65ea01 fix: keep explicit forks out of lineage report 2026-05-11 15:23:52 +08:00
ai-ag2026 4b486f2860 feat: record turn journal lifecycle events 2026-05-11 09:13:25 +02:00
Nathan Esquenazi b766b7f759 Merge pull request #2060 from nesquena/contributors-refresh-v0.51.44
docs(contributors): refresh contributor stats to v0.51.44
2026-05-11 00:03:19 -07:00
nesquena-hermes b34643b92c docs(contributors): refresh contributor stats to v0.51.44
Update CONTRIBUTORS.md and the README contributors section to reflect
130 contributors and 568 PR credits as of v0.51.44 (was 66/142 at
v0.50.245). The numbers grew because:

- The previous refresh was 1 release-cycle ago (50+ tags + 8 batch
  releases of contributor PRs ago).
- The new counting rule explicitly includes closed-but-absorbed PRs:
  PRs whose original branch shows "closed" on GitHub but whose content
  shipped via batch-release squash with a Co-authored-by trailer, or
  via salvage rewrite with CHANGELOG attribution. This better reflects
  what users actually contributed.

The compilation pipeline:

1. Pull every closed PR from gh api (state=closed, both merged and
   unmerged on GitHub) — 1421 PRs.
2. Walk CHANGELOG.md release-by-release and extract:
   - `PR #N by @user` (canonical bullet form)
   - `(#N by @user`, `(PR #N by @user`, `(#N, @user;`
   - `PRs #A, #B by @user` (plural)
   - `@user — PR #N`, `@user — N PR (#A, #B)`
   - `(credit: @user)` and `(credit: @userA and @userB)`
3. For every PR# mentioned in CHANGELOG, union the explicit @-attributed
   users with the gh PR author (when external). Maintainer accounts
   (@nesquena, @nesquena-hermes) are excluded.
4. For PRs merged on GitHub but not mentioned in CHANGELOG (very early
   PRs, non-noteworthy direct merges), credit the gh author.
5. Three salvaged-design contributors not directly in CHANGELOG are
   credited in the special-thanks roll: @indigokarasu (#213 →
   v0.50.0 design language), @andrewy-wizard (#177 → initial Chinese
   locale absorbed into v0.42.0), @zenc-cp (#133 → anti-hallucination
   guard absorbed into streaming.py).

Pre-cleaning step strips HTML entities (`&#10;` etc.) before PR# scan
to avoid false matches. PR# regex requires a whitespace/paren/bracket
preceder so identifiers like `--key=123` and `(##10`-style headings
don't pollute the count.

Per-user first/last release computed from:
- For merged-on-GH PRs: the smallest tag whose creator-date is >= the
  PR's merged_at timestamp.
- For absorbed PRs: the release section in CHANGELOG that explicitly
  attributes to the user (or the earliest release that mentions the
  PR# if no explicit attribution exists for that user).

CONTRIBUTORS.md sections:
- Top contributors (5+ PRs) — 20 people, ranked
- Sustained contributors (3–4 PRs) — 11 people
- Two-PR contributors — 14 people, flat list
- Single-PR contributors — 85 people, flat list
- How credit is tracked — four paths described
- Special thanks — 11 highlight blurbs

README contributors section trimmed to top-10 table + notable-
contribution blurbs (29 distinct contributors mentioned with concrete
PR numbers). Same data, condensed for the README.

No code changes. Docs only.
2026-05-11 06:59:42 +00:00
ai-ag2026 5cd001d545 feat: add crash-safe turn journal writer 2026-05-11 08:49:53 +02:00
nesquena-hermes f00cb74f77 Merge pull request #2058 from nesquena/stage-337
Release T (v0.51.44): 5-PR batch (#2048 + #2052 + #2053 + #2055 + #1970) + test-suite network isolation
v0.51.44
2026-05-10 23:20:29 -07:00
nesquena-hermes cd7107cefb test(infra): identity check by qname (CI re-imports conftest under multiple roots)
CI's pytest invocation imports conftest twice (once via the standard
tests/ discovery, once via repo-root rootdir discovery), producing two
distinct function objects with the same __qualname__ but different `is`
identity. The strict identity assertion failed because each import
created a fresh closure. Switch to __qualname__ substring check — same
guarantee (default-on state has the wrapper installed; fixture restores
the real one) without the multi-import sensitivity.
2026-05-11 06:18:13 +00:00
nesquena-hermes d9bc8360a4 test(infra): fixture swaps real functions via monkeypatch (CI-robust)
CI on Python 3.11 still failed test_allow_outbound_network_fixture_*
because the previous module-global toggle (_ALLOW_OUTBOUND=True/False)
was unreliable on the runner — the wrapper's global lookup at call time
sometimes saw False even after the fixture's True assignment.

Switch to monkeypatch-based fixture: instead of toggling a global that
the wrapper checks, restore socket.create_connection and
socket.socket.connect to their REAL captured implementations for the
duration of the test. Pytest's monkeypatch fixture handles teardown so
the wrappers are reinstalled automatically.

Rewrote the two paired tests to check function identity
(socket.create_connection is _hermes_blocked_create_connection vs. is
_REAL_CREATE_CONNECTION) instead of attempting a live outbound to
8.8.8.8:53 — direct identity check is hermetic and doesn't depend on
whether the CI runner has any outbound network access at all.
2026-05-11 06:15:46 +00:00
nesquena-hermes 6d83d16016 test(infra): tighten IPv6 unique-local check + replace self-passing fixture test
Two low-severity follow-ups from Opus regrounding review:

1. The IPv6 unique-local fc00::/7 check was `h.startswith('fc') or
   h.startswith('fd')` — too loose. It would also classify hostnames
   like 'food.example.com' or 'fdsa.test' as 'local' and silently let
   them through the block. Tightened to a regex match for canonical
   IPv6 syntax (`f[cd][0-9a-f]{0,2}:`) so only actual IPv6 addresses
   match. Same fix in both tests/conftest.py and server.py.

2. test_allow_outbound_network_fixture_unblocks was technically
   self-passing: it tried to connect to a *.invalid hostname, which is
   in the allow-list, so the real socket.create_connection would run
   regardless of whether the fixture toggled the block. Replaced with
   a public-IP-based test that actually proves the toggle works, plus
   a paired test_block_is_active_outside_the_fixture sanity test that
   proves the block is on without the fixture.

Both follow-ups noted by Opus advisor as 'defer-OK' but trivial fixes
so landing them in this batch.
2026-05-11 06:12:07 +00:00
nesquena-hermes 23cfc99738 fix(config): split hermes_cli and urlopen fallback in lmstudio branch (CI fix)
CI on Python 3.13 (clean editable install, no hermes_cli package) was still
failing the 3 lmstudio tests after the first fix attempt. Root cause: the
outer try/except in the lmstudio branch was catching ImportError from
`from hermes_cli.models import provider_model_ids`, hijacking the whole
branch and silently skipping the urlopen fallback.

Restructured into two independent tiers:
  1. hermes_cli lookup in its own try/except — ImportError logs at DEBUG
     and continues with lm_ids=[].
  2. urlopen fallback runs unconditionally when lm_ids is empty, including
     after hermes_cli import failure.

New regression test `test_lmstudio_fallback_works_when_hermes_cli_unavailable`
explicitly blocks hermes_cli via sys.meta_path and verifies the lmstudio
group still populates from the urlopen fallback. Without this test, the
CI-vs-local divergence (local env had hermes_cli installed, CI didn't)
would keep slipping through.

All 12 lmstudio-related tests pass, including the 3 #1527 tests that
broke on stage-337.
2026-05-11 06:06:58 +00:00
nesquena-hermes 1819ead93d docs: CHANGELOG v0.51.44 Release T (5-PR batch + test network isolation) 2026-05-11 06:03:12 +00:00
nesquena-hermes 12cef733e3 fix(recovery): preserve worktree metadata + workspace + message_count on state.db sidecar rebuild
PR #2053 added worktree-backed session creation. PR #2041 (shipped in
v0.51.42) added state.db sidecar reconciliation that rebuilds a missing
<sid>.json sidecar from the canonical state.db row when the JSON file is
gone (failed save, manual rm, restore-from-backup with mismatched dirs).

The two interact silently. `_state_db_row_to_sidecar()` was hard-coding
`'workspace': ''` and never propagating the four worktree_* fields from
the row to the rebuilt sidecar dict. So a worktree-backed session that
loses its sidecar and gets rebuilt from state.db:

- loses `worktree_path` → matches the empty-session sidebar filter at
  `api/models.py:1067/1107` (which spares worktree-backed empty sessions
  via `not s.get('worktree_path')`) → session disappears from the
  sidebar even though the worktree directory still exists on disk.

- loses `workspace` → downstream tools (terminal panels, file pickers
  that use `s.workspace`) operate on empty string instead of the original
  worktree path.

- always reports `message_count == 0` → contributes to the empty-session
  filter even for sessions that have messages in `state.db.messages`.

Fix:

1. `_read_state_db_missing_sidecar_rows()` SELECT now includes
   `workspace, worktree_path, worktree_branch, worktree_repo_root,
   worktree_created_at, message_count` (each gated by
   `_sql_optional_col()` so older state.db schemas without those columns
   continue to work — recovery degrades gracefully rather than 500ing).

2. `_state_db_row_to_sidecar()` propagates each field. workspace comes
   from the row if it's a string, otherwise '' (matching pre-fix behavior
   for non-worktree sessions). message_count comes from the row if
   it's an int, otherwise falls back to `len(messages)` so the rebuilt
   sidecar always has a coherent count.

3 new regression tests in tests/test_state_db_worktree_recovery.py
exercise:
- worktree session with messages → all four worktree_* fields preserved.
- non-worktree session → worktree_* fields all None (no spurious
  propagation), workspace=''.
- empty worktree session (the worst case) → confirms the rebuilt sidecar
  does NOT match the empty-session-exempt filter, so it stays visible
  in the sidebar.

Caught by Opus advisor during stage-337 review (the cross-PR interaction
between #2053 and the previously-shipped #2041 wasn't exercised by either
PR's individual test suite).
2026-05-11 06:00:13 +00:00