Commit Graph

2113 Commits

Author SHA1 Message Date
Gordie fb8bf2f5bd fix(webui): keep #1855 fast-path window and Copilot gpt-4o regression tests green
- Extract the MoA @moa:/moa/ prefix-stripping branch of
  _resolve_compatible_session_model_state into a new
  _moa_fast_path_model_state() helper. Inlining it had pushed
  `catalog = get_available_models()` just past the 6000-char
  source window that
  tests/test_issue1855_resolve_model_provider_fast_path.py::
  TestFastPathSourceShape scans to guard the #1855 fast-path/
  catalog-call ordering, breaking that regression test after
  rebasing onto current master.
- Re-add gpt-4o to the refreshed Copilot static fallback list.
  It's a real Copilot-served model and
  tests/test_issues_373_374_375.py::TestStaleModelListCleanup::
  test_copilot_list_unchanged asserts the Copilot list keeps it
  even as #374 removes it from the generic OpenAI list. The live
  Copilot catalog probe remains authoritative; this only affects
  the cold-start/probe-miss fallback.

Verification:
  ./scripts/test.sh tests/test_issue1855_resolve_model_provider_fast_path.py tests/test_issues_373_374_375.py tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
  -> 73 passed in 3.60s

  ./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
  -> 1745 passed, 2 skipped, 9775 deselected, 1 xpassed in 68.57s
2026-07-01 07:01:51 -05:00
Gordie 0b8e67c20d fix(#5301): admit active models-only custom provider without reopening dedup regression
Maintainer review found regression 2 on commit 575996a7: the new
_has_provider_route gate (api/config.py) required api/base_url/api_key/key_env
before admitting a configured provider into the picker, which dropped a
models-only custom provider config (the lmstudio-style shape from #1970,
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_preserves_dict_shaped_raw_key_lookup).

A naive fix (admit any config with models) would re-break
test_unknown_duplicate_copilot_provider_config_is_not_rendered, since a
spurious alias like copilot-2: {name: copilot, models: {...}} must stay
rejected.

Fix: admit a models-only provider config as evidence only when its canonical
id matches the active/configured provider (threaded via active_provider,
already in scope). Non-active models-only configs (including duplicate
aliases of known providers) are still rejected.

Added a regression test:
tests/test_pr1970_lmstudio_base_url_fallback.py::test_provider_catalog_rejects_non_active_models_only_custom_provider
covering both the active-admits and non-active-rejects cases side by side.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py tests/test_pr1970_lmstudio_base_url_fallback.py -q
26 passed, 1 skipped in 5.58s

Broader sanity pass: ./scripts/test.sh tests/ -k "config or provider or model or copilot or moa or lmstudio" -q
1728 passed, 5 skipped, 9654 deselected (4 pre-existing failures confirmed unrelated: 2 reproduce identically on the pre-fix commit via git stash, 2 are order-dependent and pass in isolation).
2026-07-01 07:01:51 -05:00
nesquena-hermes cf1ab5bbbc fix(#5301): scope models-as-settings-map guard to Copilot only (was breaking providers.<id>.models allowlist for all built-ins, #644 regression) 2026-07-01 07:01:51 -05:00
Gordie 07a1509feb fix(webui): harden MoA picker preset fallbacks
- Avoid an unguarded resolve_moa_preset fallback call when preset resolution fails.
- Populate the MoA picker directly from configured presets so older Hermes Agent installs that lack provider_model_ids('moa') still render the virtual provider.
- Add regression coverage for both review findings.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py -q
12 passed, 1 skipped
2026-07-01 07:01:51 -05:00
Gordie f29441bf09 fix(webui): split MoA picker and Copilot catalog fixes
- Keep resolve_moa_preset optional so older hermes-agent installs still degrade gracefully.
- Surface MoA presets in the WebUI model picker as a virtual provider.
- Keep configured Copilot per-model settings from collapsing the built-in catalog.
- Refresh the Copilot static fallback used when the live catalog probe misses.

Verification:
./scripts/test.sh tests/test_issue5057_moa_webui_route.py tests/test_copilot_provider_model_settings_not_allowlist.py tests/test_moa_model_picker_provider.py -q
11 passed, 1 skipped
2026-07-01 07:01:51 -05: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