Commit graph

2110 commits

Author SHA1 Message Date
nesquena-hermes
6caaced8eb fix(webui): bound in-memory SESSIONS cache with lazy reload (#4765)
Root cause of the silent-crash-after-hours cluster (#4765/#2233/#4633): the
global in-memory SESSIONS LRU evicted with a blind popitem(last=False), which
could drop an actively streaming or not-yet-persisted session (data loss) and
was capped only via an env var. On long-running installs the effective result
was unbounded RAM growth until segfault.

- Add _session_is_evictable(): a session is evictable ONLY when it is not
  streaming (no active_stream_id), has no in-flight turn (no pending_user_message
  / pending_started_at), and its full state is proven on disk (sidecar
  message_count >= in-memory count; metadata-only stubs and zero-message shells
  are trivially safe).
- Add _evict_sessions_over_cap(): replaces all 8 blind popitem loops across
  models.py, routes.py, streaming.py. Walks the LRU oldest-first and removes only
  provably-safe entries; never acquires LOCK/stream locks itself (caller holds
  LOCK) so no lock-ordering deadlock. May briefly exceed the cap rather than ever
  evict an active/unsaved session.
- Make the cap configurable via config.yaml webui.sessions_cache_max
  (get_sessions_cache_max()); precedence config.yaml -> HERMES_WEBUI_SESSIONS_MAX
  (legacy) -> DEFAULT_SESSIONS_CACHE_MAX=300. No new HERMES_* env var. Invalid or
  <1 values fall back so a typo can never disable the bound.
- Evicted sessions lazily reload from their JSON sidecar via the existing
  get_session() accessor; no call sites changed. _index.json sidebar behavior
  unchanged (the sidebar reads the index, not SESSIONS).
- Add tests/test_issue4765_sessions_lru_eviction.py (8 tests): eviction past
  cap, active/streaming never evicted, unsaved/stale-tail never evicted, lazy
  reload with identical content, and no-data-loss under heavy churn.
- README: document the config.yaml key + safety semantics.

Fixes #4765.
2026-07-01 18:14:39 +00:00
nanw
161e4173c2 fix(sessions): prune index-only ghost sessions (#5331)
Phase 2 of cleanup now sweeps _index.json for stale entries with no
backing file and no in-memory session, removing them regardless of
title (catches multi-language 'Untitled' variants). Phase 3 only
deletes the index when Phase 1 removed files AND Phase 2 couldn't
run, fixing the cache-busting on every cleanup call.

Fix two phase-interaction bugs flagged by Greptile review:
- Track phase1_removed_ids to prevent Phase 2 double-counting
  sessions already removed from disk by Phase 1.
- Track phase2_rewrote_index so Phase 3 skips deletion when
  Phase 2 already cleaned the index in-place.

Adds test_issue5331_index_only_ghost_cleanup.py with 12 test cases.
2026-07-01 17:41:31 +08:00
nesquena-hermes
b65ab14538 fix(#5307): gate /api/goal + /api/btw subagent write paths (Codex round 11) 2026-07-01 08:55:58 +00:00
nesquena-hermes
cdbd84585a fix(#5307): gate final 4 subagent write paths (Codex round 10)
_session_is_subagent_view_only guard added to the remaining paths that could
mutate a stale persisted subagent sidecar:
- _handle_handoff_summary (appends tool msg to state.db)
- _handle_chat_sync (fallback POST /api/chat)
- /api/session/draft POST (composer_draft save)
- /api/personality/set (per-session personality save)
Fixes #5307
2026-07-01 08:47:34 +00:00
nesquena-hermes
e6e7454c7a fix(#5307): gate ALL remaining session-mutation routes + stale-webui-row coercion (Codex round 9)
Enumerated every /api/session/<mutation> route and applied _session_is_subagent_view_only:
duplicate, branch, retry, undo, toolsets, compress, archive (existing-sidecar path)
now 400 for subagent children; delete/clear/truncate/pin already gated; rename/move/update
route through the gated _get_or_materialize_session (403). This closes the data-loss
paths (delete_cli_session, retry/undo truncate, branch/duplicate fork).

Also: /api/sessions list coercion now also honors state.db source='subagent' for rows
whose stale index says webui/fork, so they can't surface as writable/CLI sidebar rows.
Fixes #5307
2026-07-01 08:38:47 +00:00
nesquena-hermes
23743c5976 fix(#5307): guard direct mutation routes + coerce list rows (Codex round 8)
The root non-CLI classification surfaced subagent rows in the sidebar without
read_only, exposing delete/truncate/pin — and /api/session/delete calls
delete_cli_session() which erases the child's state.db transcript (data loss).

- /api/sessions list: coerce subagent rows to read_only=True + is_cli_session=False
  so the UI offers no mutation affordances.
- New _session_is_subagent_view_only() shared guard; applied to the direct
  mutation routes that bypass _get_or_materialize_session(): delete / clear /
  truncate / pin now 400 for subagent children. (rename/move already route
  through the gated _get_or_materialize_session and 403.)
- tests: guard helper + static contract that all 4 routes + list coercion present.
Fixes #5307
2026-07-01 08:21:03 +00:00
nesquena-hermes
9eb208ebc3 fix(#5307): root-fix subagent classification + GET serialization (Codex round 7)
- api/agent_sessions.py is_cli_session_row(): add 'subagent' to non_cli_sources
  so EVERY consumer (sidebar rows, /api/sessions, etc.) classifies a delegated
  child as non-CLI (the root the per-site fixes were compensating for).
- api/routes.py GET /api/session happy path: coerce is_cli_session=False +
  read_only=True in the serialized payload for a subagent child before redaction,
  so a stale writable sidecar can't be exposed as writable to the browser.
Fixes #5307
2026-07-01 08:10:24 +00:00
nesquena-hermes
2e33e40272 fix(#5307): guard persisted subagent sidecars against writable use (Codex round 6)
- _get_or_materialize_session happy path: reject an already-persisted subagent
  sidecar (source_tag/raw_source=='subagent' or state.db subagent) even when it
  was stored read_only=False (pre-fix materialization), so chat-start can't write it.
- import_cli existing-session refresh: coerce read_only=True on the persisted
  sidecar (and response) when it's a subagent child.
- test: persisted read_only=False subagent sidecar is still refused by the helper.
Fixes #5307
2026-07-01 08:04:47 +00:00
nesquena-hermes
6e2ead5b1b fix(#5307): close cross-profile + existing-session subagent edges (Codex round 5)
- import_cli _read_only_view now also treats resolved cli_meta source_tag/raw_source
  =='subagent' as view-only (all_profiles=true resolves cli_meta from a non-active
  profile, which the active-profile state.db _sa_child check could miss).
- import_cli existing-session refresh branch no longer hardcodes is_cli_session=True
  for a subagent child (both the persisted update and the response payload).
Fixes #5307
2026-07-01 07:55:00 +00:00
nesquena-hermes
161fdeba7a fix(#5307): gate the 3rd/final import_cli_session write path (archive fallback)
Codex round 4 found POST /api/session/archive's missing-sidecar fallback also
calls import_cli_session(). grep confirms exactly 3 import_cli_session() call
sites in routes.py; all 3 now refuse source='subagent' children:
  - 4792 _get_or_materialize_session (chat-start) -> PermissionError
  - 22382 import_cli endpoint -> read-only view payload
  - 13885 archive fallback -> 400 'Subagent sessions cannot be archived'
So no path can materialize a delegated child as a writable WebUI sidecar.
Fixes #5307
2026-07-01 07:48:34 +00:00
nesquena-hermes
c989acc991 fix(#5307): gate subagent children in the shared materialize chokepoint (Codex round 3)
Codex found a 3rd writable path: POST /api/chat/start -> _get_or_materialize_session()
materialized source='subagent' as a writable sidecar before the not_claimable guard.
Fix: refuse subagent children (PermissionError) at that shared chokepoint, checked via
_is_subagent_child_session_id(sid) (state.db source, independent of cli_meta) BEFORE the
materialize path — so all three entry points (GET synth, import_cli, chat-start) now
consistently keep a delegated child view-only. CLI/TUI/Desktop materialization preserved.

Tests: materialize helper refuses subagent child + still allows tui.
Fixes #5307
2026-07-01 07:44:11 +00:00
nesquena-hermes
3ecb2625aa fix(#5307): close 2 more subagent-child writable-session holes (Codex round 2)
- api/routes.py GET /api/session synthesized response: stop hardcoding
  is_cli_session=True; serialize bool(synth.is_cli_session) so a recovered
  subagent child stays not-CLI-classified (was overriding the helper's False).
- api/routes.py POST /api/session/import_cli: gate source='subagent' into the
  read-only view payload (is_cli_session=False, imported=False) BEFORE
  import_cli_session(), so a subagent child can never be materialized as a
  writable WebUI sidecar via this endpoint.
- tests: assert import_cli routes subagent children read-only (no materialize).

Both were paths that bypassed the _is_claimable_cli_source denylist. Fixes #5307
2026-07-01 07:35:48 +00:00
nesquena-hermes
a823ab5ff8 fix(#5307): recover subagent child transcript view-only (Codex hardening)
Reworked per the Codex gate finding: the first approach widened the client
import predicate, which (a) conflicted with #3603's intentional _isExternalSession
gate and (b) let import_cli persist the subagent child as a WRITABLE, CLI-classified
session that then passed the poll-skip/active-refresh gates.

Corrected to a server-side, view-only recovery:
- api/routes.py: mark source='subagent' as NON-claimable in _is_claimable_cli_source
  (both cli_meta and state.db source denylists). A subagent child now resolves via
  the not_claimable branch -> read-only Session with its state.db transcript, and
  build_session takes is_cli_flag (False for subagent children) so the recovered
  session is NOT CLI-classified and can't widen the frontend _isExternalSession gates.
- static/sessions.js: REVERTED to master (no client change needed; #3603 contract intact).
- tests: assert reason=not_claimable, read_only=True, is_cli_session!=True, transcript present;
  #2782 deleted-webui 404 preserved; #3603 _isExternalSession contract preserved.

58 tests green (5307 + 3603 + claim-cli + core-data-loss). Fixes #5307
2026-07-01 07:24:16 +00:00
nesquena-hermes
bed98c787c fix(sessions): load delegated subagent child transcript from state.db (#5307)
A delegated subagent child (source='subagent' in state.db) usually has no WebUI
sidecar but is registered in the WebUI index sharing the parent's lineage. That
made GET /api/session -> _claim_or_synthesize_cli_session return 'was_webui' ->
404, so the child pane opened empty despite state.db holding messages.

- api/routes.py: add _state_db_session_source() + _is_subagent_child_session_id();
  exclude subagent children from the was_webui 404 gate so they recover their
  state.db transcript (the #2782 self-heal 404 for deleted WebUI sessions is kept).
- static/sessions.js: add _isSubagentChildSession() + _sessionNeedsServerImportForLoad()
  (kept separate from _isExternalSession to avoid widening refresh-gating); the
  main session-tap, lineage-segment, and child-open handlers now trigger the
  import/merge path for subagent children.
- tests: test_5307_subagent_child_transcript.py (7 tests).

Fixes #5307
2026-07-01 07:15:19 +00:00
nesquena-hermes
d6c01245c5 stage #5024 2026-07-01 06:49:44 +00:00
nesquena-hermes
36436c9397 stage #5012 2026-07-01 06:45:22 +00:00
nesquena-hermes
9973b36cce stage #5268 2026-07-01 06:41:09 +00:00
Paladin173
d8f0f8f97f Fix composer control reorder rebase collision
Reapply footer control ordering on current upstream/master while preserving the required situational chip renderer. Persist composer_control_order with backend validation, keep the settings descriptions reorder-aware, and make primary/situational chip renderers participate in same-group drag ordering.

Verified with: node --check static/boot.js; node --check static/panels.js; git diff --check; ./scripts/test.sh tests/test_issue4598_composer_control_visibility.py
2026-06-30 21:11:54 -05:00
Rod Boev
2ea5fe06fa fix(#3825): harden oidc endpoint and claim validation 2026-06-30 20:59:19 -04:00
nesquena-hermes
d257e5f36c fix(security): gate embedded-terminal endpoints to local origins when auth disabled
The embedded workspace terminal spawns a PTY shell that runs arbitrary
commands as the server-process user. check_auth() returns True
unconditionally when no password/passkey is configured (the default
out-of-the-box state), so without a network-scope gate the terminal
endpoints were reachable by any unauthenticated caller able to hit the
port — which on a passwordless public bind is remote code execution.

Apply the same local-origin gate the onboarding/bootstrap endpoints use
(_onboarding_gate_allows) to /api/terminal/{start,input,resize,close} and
/api/terminal/output: with auth disabled, accept only loopback/private
origins, ignore spoofable X-Forwarded-For/X-Real-IP unless
HERMES_WEBUI_TRUST_FORWARDED_FOR=1, and honor HERMES_WEBUI_ONBOARDING_OPEN=1
as the explicit opt-out for a deliberately-exposed server. Auth-enabled
servers (cookie already verified upstream) and genuine same-host clients
are unaffected.

Also fixes a latent test-isolation leak in
test_extension_route_remains_behind_webui_auth: it set HERMES_WEBUI_PASSWORD
but never invalidated the process-wide password-hash cache, so its result
depended on suite execution order (exposed when the new test file shifted
ordering). Invalidate before+after so it reads the env var deterministically.

12 new gate tests in tests/test_cvd3_terminal_local_origin_gate.py.
2026-07-01 00:40:25 +00:00
nesquena-hermes
9bd71e9bae Merge #4988 (enihcam prune orphan zero-msg sessions) into stage
# Conflicts:
#	CHANGELOG.md
2026-07-01 00:19:56 +00:00
nesquena-hermes
3e84d7bf11 Merge #4738 (savagebread neon skins + skin-persistence fix) into stage 2026-06-30 22:21:28 +00:00
nesquena-hermes
0b2507ac9f fix(#4738): register neon-soft/neon-paint in _SETTINGS_SKIN_VALUES (server-side skin persistence) 2026-06-30 22:13:36 +00:00
nesquena-hermes
78ca4d43a1 Merge #5005 (futureworld678 Shift+Enter) into stage 2026-06-30 21:51:42 +00:00
nesquena-hermes
0b74397fa6 Merge #5120 (rodboev budget limit) into stage 2026-06-30 21:28:51 +00:00
nesquena-hermes
8a5172275d Merge #5002 (per-project quick-create, opt-in) into stage 2026-06-30 20:56:05 +00:00
nesquena-hermes
ba401d3784 Merge #5079 (OpenAI TTS + SSRF hardening) into stage 2026-06-30 20:39:04 +00:00
nesquena-hermes
eb2c4b1e7d fix(#5079): no-redirect opener for OpenAI TTS (block redirect-to-private SSRF + bearer leak)
A public TTS host could 301/302/303-redirect POST /audio/speech to an internal
target (e.g. http://169.254.169.254), and urllib's default redirect handler
would follow it carrying the Authorization bearer — both an SSRF bounce past the
base-url guard and a credential leak. Now uses a no-redirect opener
(_NoRedirectTtsHandler raises on any redirect) via the _tts_open seam. +redirect
rejection regression test. Residual DNS-rebinding TOCTOU (re-resolve at connect)
is a narrower low-severity window noted for follow-up.
2026-06-30 20:37:43 +00:00
nesquena-hermes
6bcb728f08 fix(#5079): block private/link-local/reserved IP targets in OpenAI TTS base_url (SSRF hardening)
The base_url validator accepted any https host; an https URL pointing at an
internal/link-local/loopback/reserved IP (e.g. https://169.254.169.254 cloud
metadata, https://10.x internal) passed the scheme-only check. Now resolves the
host and rejects blocked-target addresses (private/loopback/link-local/reserved/
multicast/unspecified), while still allowing public OpenAI-compatible hosts and
the explicit localhost-over-http dev case. DNS-resolution failure is allowed
(unreachable host can't be an SSRF vector + avoids false-rejecting public hosts
that don't resolve in sandboxed envs). +5 regression vectors.
2026-06-30 20:37:43 +00:00
nesquena-hermes
b098e5c658 feat(#4676): gate per-project quick-create buttons behind opt-in setting
Add a default-OFF 'project_quick_create_buttons' setting that gates the
per-project '+' quick-create buttons on sidebar project chips. The chip
render site only calls _attachProjectQuickCreateButton when the new
window._projectQuickCreate runtime flag is enabled, so the buttons do
not appear unless the user opts in via Settings.

Mirrors the large_text_paste_as_attachment opt-in plumbing (defaults,
allowlist, settings collect/restore/save, window hydration at both sites,
checkbox row, i18n across all locales).
2026-06-30 19:49:07 +00:00
Lester
b1c11939aa Add Shift+Enter send key option 2026-06-30 18:04:29 +00:00
nesquena-hermes
121059b384 Merge #5161 (pr/5144-busy-composer-placeholder-hint) into stage-w5 2026-06-30 17:25:03 +00:00
allenliang2022
4ef9d7c5cb fix(webui): render a server-initiated turn that finished during an SSE gap on a visible tab
A visible tab can permanently miss a server-initiated turn (self-wake /
cron / restart hook) when its per-session SSE is momentarily down at the
emit instant. server_turn_started is a fire-and-forget broadcast with no
replay buffer; if the turn also finishes before the tab reconnects, the
on-subscribe self-heal finds no live run in ACTIVE_RUNS and replays
nothing, so the transcript stays stale until a hard refresh.

Extend the self-heal: the (re)subscribing tab reports its last-known
message_count (?known_count); when there is no live run to replay, the
handler compares the persisted count and, if the server is ahead, emits a
lightweight session-updated frame. The frontend syncs incrementally via
the #5189 keepStaleUntilLoaded swap-in-place loadSession path (no
clear+refetch, so the #5177/#5189 blank-gap jump is not reintroduced).
Idle-only and pane-scoped; emits only when the server is strictly ahead.
2026-06-30 17:13:45 +00:00
Maude Bot
5263b9acf7 feat: add Webhooks sidebar filter parity
Co-authored-by: Kilian Tyler <kilian@kil.dev>
2026-06-30 17:06:31 +00:00
nesquena-hermes
3a12ac6bb8 Merge #5264 (fix/large-text-paste-toggle-upstream) into stage-w2 2026-06-30 16:35:55 +00:00
nesquena-hermes
b76264c310 Merge #5272 (fix/5141-terminal-failure-transcript-evaluator) into stage-w1 2026-06-30 16:19:47 +00:00
Rod Boev
5f07e12387 fix(#5270): route the continuity refresh through chat start 2026-06-30 08:33:20 -04:00
Rod Boev
cbe36c0ada fix(#5270): confine continuity repair to the chat path 2026-06-30 08:18:43 -04:00
Rod Boev
7e0936f3f1 fix(#5270): preserve first WebUI continuation context for CLI sessions 2026-06-30 07:47:57 -04:00
nankingjing
c2bb4c655d refactor(streaming): split terminal-failure transcript evaluator (#5141)
Extract _turn_transcript_lacks_final_assistant_answer() so settlement
logic can inspect an already-merged transcript. The merge wrapper now
delegates to the pure evaluator without changing behavior.

Fixes #5141
2026-06-30 18:40:08 +08:00
justanotherAIcontributor
91d32c40c5
Merge branch 'master' into fix/large-text-paste-toggle-upstream 2026-06-30 16:13:49 +10:00
Rod Boev
51a0fca24a fix(#5092): continue WebUI goals after gateway turns 2026-06-30 05:04:32 +00:00
nesquena-hermes
468b61e513 fix(auth): release STREAM_SESSION_OWNERS on worker early-return + stale-run prune
Gate hardening for #5198: the new stream-owner map leaked entries on three
paths that bypass the teardown finally — worker early-return when the stream
was cancelled before startup (streaming.py, gateway_chat.py) and the direct
ACTIVE_RUNS stale-zombie prune (routes.py). Each now calls unregister_stream_owner.
Adds a regression test proving the early-return path no longer leaks.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-30 04:39:00 +00:00
Hermes Agent
fabd461dcd fix(auth): guard profile-owned stream IDs 2026-06-30 04:28:15 +00:00
Hermes Agent
a993fee060 docs(auth): document request session guard scope 2026-06-30 04:28:15 +00:00
Hermes Agent
f26896f96a refactor(auth): remove unreachable chat retag branch 2026-06-30 04:28:15 +00:00
Hermes Agent
9e861b5651 fix(auth): tolerate query-less route inputs 2026-06-30 04:28:15 +00:00
Hermes Agent
c068dbc39b fix(auth): enforce active-profile session IDs 2026-06-30 04:28:15 +00:00
Stacey2911
6b0d8eaadf fix(webui): make large text paste-as-attachment toggleable via settings
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.
2026-06-30 14:25:53 +10:00
hinotoi-agent
41ec8238ba chore: log file manager profile denials 2026-06-30 04:12:27 +00:00