610 Commits

Author SHA1 Message Date
nesquena-hermes
e8529c84e5 Merge #4060: pass custom provider keys to context probes (#4059) 2026-06-13 00:17:16 +00:00
nesquena-hermes
6ec5527386 harden(#4057): pass local skills dir to _skill_category_from_path explicitly (Opus SHOULD-FIX)
_active_skill_search_dirs filters to existing dirs, so on a host with no local
skills dir but configured external dirs the local root is dropped from the list
and the position-based skills_dirs[0]==local assumption misclassified the first
external root as local (its flat skills silently lost their category label).
Pass the local dir explicitly (backward-compatible optional param defaulting to
the old skills_dirs[0] behavior) + regression test for the absent-local-dir case.
2026-06-12 20:28:46 +00:00
nesquena-hermes
2fea2d588b Merge #4058 into stage-mj 2026-06-12 20:14:42 +00:00
b3nw
9ac9f8d690 fix(#4059): log unresolved custom provider key templates 2026-06-12 16:23:35 +00:00
b3nw
b2eda41b49 fix(#4059): pass custom provider keys to context probes
Resolve custom provider API keys from the matched config snapshot and pass them through session hydration plus streaming fallback context-length probes. This prevents authenticated /v1/models endpoints from falling back to the default 256K window and clobbering larger persisted session metadata.
2026-06-12 15:53:48 +00:00
Benoît Vézina
8003faa801 feat(tts): add French (fr-CA, fr-FR) voices to Edge TTS allowlist
Adds 4 fr-CA + 3 fr-FR Edge neural voices so francophone users can use
the Edge TTS engine instead of receiving HTTP 400 "invalid voice" on
every utterance. Pure superset of the existing allowlist; no validation
or rate-limit behavior changes.

Tests mirror the test_issue2931 in-process / mocked-edge_tts pattern:
each new voice is parametrized through _handle_tts and asserted to
reach synthesis (HTTP 200); fr-BE-CharlineNeural (real Edge voice
but intentionally unlisted) is asserted to still 400.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 09:44:24 -04:00
Rod Boev
ddce08fd70 docs(skills): pin the local-first search-dir contract 2026-06-12 09:36:10 -04:00
Rod Boev
1f42f3190f fix(skills): categorize flat external skill dirs (#4053) 2026-06-12 09:21:47 -04:00
nesquena-hermes
aef15ca559 Merge #4023 — bind active profile cookie to session (#803) 2026-06-12 07:19:46 +00:00
ai-ag2026
de3b756558 fix: merge missing webui lineage display rows 2026-06-12 08:00:11 +02:00
hinotoi-agent
4dca506858 Bind profile cookie to auth session 2026-06-12 10:52:56 +08:00
Rod Boev
a82eb1622f fix(#3958): keep reasoning effort status on the active session model 2026-06-11 18:46:17 -04:00
nesquena-hermes
7a5c3bc374 fix(#3994): scope happy-path guard to read_only flag; complete #1436 mock stub
Refined the Codex-CORE fix: the happy-path (already-stored session) guard rejects
only an explicit read_only flag — a stored messaging session already owns its
sidecar, so the messaging-fork risk is specific to the materialize FALLBACK
(which creates a sidecar) where the _is_messaging_session_record check stays.
Also completed test_issue1436's _stub_session MagicMock (read_only=False,
_loaded_metadata_only=False) — bare MagicMock auto-attrs were truthy, tripping
the new read-only guard + _ensure_full_session_before_mutation reload.
2026-06-11 21:45:36 +00:00
nesquena-hermes
6f41fe7490 fix(#3994): reject read-only + messaging sessions on BOTH happy-path and materialize fallback (Codex CORE x2)
Codex caught two data-integrity gaps in the contributor's guard: (1) the
get_session() happy path returned a stored session without checking read_only/
messaging, so an already-imported read-only session could be mutated via
rename/update/move; (2) the fallback only checked cli_meta.read_only, but agent
rows normalize messaging sources WITHOUT setting read_only — materializing a
writable sidecar for a state.db-owned messaging session forks its title/state.
Now reject getattr(s,'read_only') OR _is_messaging_session_record on the happy
path, and cli_meta.read_only OR _is_messaging_session_record(cli_meta) in the
fallback. Replaced the messaging-stub test with 3 regression tests (stored
read-only, stored messaging, messaging cli_meta without read_only flag).
2026-06-11 21:41:51 +00:00
Dima Diall
46f87028c2 fix(routes): materialize CLI sessions on rename/move/update (fixes #3985)
When a session exists in Hermes Agent state.db but has no WebUI sidecar
(SESSION_DIR/{sid}.json), mutation routes (rename, move, update) would
return 404 "Session not found" despite the session appearing in the sidebar.

This mirrors the existing fallback in /api/session/archive:
- Try get_session() first (WebUI store)
- On KeyError, look up CLI metadata via _lookup_cli_session_metadata()
- For messaging/Claude Code (read_only): return 403 instead of silent 404
- For regular CLI sessions: import_cli_session() to materialize sidecar
- Preserve source_tag/raw_source/session_source/etc. for lineage

Routes updated:
- /api/session/rename
- /api/session/update (workspace switch)
- /api/session/move

Refs: #3746 (same class: session discovery vs mutation mismatch),
#3915 (session store empty but data exists in Agent store)
2026-06-11 17:06:16 +01:00
Hermes Agent
f2ef2851d3 Release LV stage — v0.51.358 (#3964 first-run password bootstrap local gate)
#3964 [security] gate first-password bootstrap (_set_password on POST /api/settings
while auth disabled) to local clients — blocks remote unauth first-run ownership.
Uses request-start auth snapshot (auth_enabled_before), so no mid-request TOCTOU.
Self-rebased onto v0.51.357 (8-behind, 3-dot fidelity verified byte-identical).

#3970 (oauth single-flight) DROPPED from this stage: Codex+Opus both caught a
check-then-insert race — _pending_oauth_flow_for releases the lock before the
device-code request + flow insertion, so concurrent unauth starts still spawn
multiple workers (Codex empirically reproduced w/ 2 threads). Returned to author
w/ the atomic per-(provider,home) start-lock fix. Re-gating the (N-1) stage.

Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
2026-06-11 04:39:34 +00:00
Hermes Agent
b5d382a3bb fix(profiles): scope the detached models-rebuild worker to the active profile (#3957)
Codex + Opus both independently caught a CORE gap in the first cut: the
bounded /api/models rebuild runs on a detached 'models-catalog-rebuild' daemon
thread that inherits neither the request-profile thread-local (#798) nor
os.environ. So on a non-default profile the worker probed the DEFAULT profile's
credentials and, when the 4s budget was exceeded, published the rebuilt catalog
to the DEFAULT profile's disk cache (cross-contamination) — exactly the slow
path a non-default cold rebuild takes.

Fix:
- profile_scope_for_detached_worker(profile_name): sets the request-profile TLS
  AND applies the profile .env on the worker thread, restoring both on exit
  (no-op for default). Distinct from profile_env_for_active_request (which reads
  the current thread's TLS and must not clear it).
- get_available_models() captures the active profile on the request thread and
  wraps the rebuild worker body (probe + over-budget publish + disk save +
  fingerprint) in that scope; the legacy synchronous rebuild applies the profile
  env on the foreground. /api/models route no longer wraps (the work moved into
  get_available_models so ALL callers — chat/start, resolution — are fixed).
- 2 new regression tests incl. the worker-thread before/inside/after assertion.

Empirically verified: a fresh worker thread resolved models_cache.json/default
WITHOUT the scope (the bug) and models_cache.work.json + the work .env + the
work auth.json WITH it.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-11 01:41:24 +00:00
Hermes Agent
70d391c44a fix(profiles): scope /api/providers + /api/models to the active profile (#3957)
On a non-default profile, Settings → Providers timed out and the model
picker showed only the default profile's models. WebUI profile switching is
per-client/cookie-scoped (#798), but two read-only paths resolved from the
process-global default profile:

- Facet A: /api/providers + /api/models did not apply the active profile's
  .env around the read, so get_auth_status() / provider_model_ids() / custom
  key lookups resolved the default profile's credentials. On a non-default
  profile the auth probes could stall past the 30s frontend abort.
- Facet B: the /api/models disk cache was a single import-time
  STATE_DIR/models_cache.json shared across every profile, while the cache
  fingerprint is profile-specific -> a non-default profile rejected the shared
  snapshot every read and cold-rebuilt (the slow serial-probe path).

Fix:
- api.profiles.profile_env_for_active_request(): applies the active
  per-request profile's .env for the duration of the read (delegates to the
  existing profile_env_for_background_worker used by streaming). No-op for the
  default/root profile, so single-profile deployments are byte-identical.
- api.config._get_models_cache_path(): profile-keys the disk cache filename
  (models_cache.<profile>.json) derived from the default path; default profile
  keeps models_cache.json unchanged (no file migration).
- routes.py: wrap both GET handlers in profile_env_for_active_request.
- conftest: restore api.profiles._active_profile + clear request-profile TLS
  after each test (a pre-existing isolation hole that profile-keyed cache
  paths newly surface under sharding).

Tested: 10 new regression tests; live before/after on isolated servers shows
a non-default profile now surfaces its configured provider (deepseek) with
its own credentials + its own cache file, where master showed only the
default profile's providers and one shared cache.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-11 01:17:22 +00:00
nesquena-hermes
cdd76c04b1 fix(model-resolver): preserve @provider:model picks across cold catalogs
_resolve_compatible_session_model_state() no longer reverts an explicit
@provider:model selection to the default when the provider's group is missing from
the cached catalog snapshot. explicit picks always honored; non-explicit (2nd+ turn
/ chat switch) preservation requires the provider to be KNOWN/CONFIGURED via the new
_provider_is_known_or_configured() (static registry + custom-provider config, NOT the
cold catalog) — so a cold live-discovery provider (ollama-cloud/deepseek/xai) is
preserved while a genuinely-unknown provider (@removed:...) falls through to
default-repair. A known-but-unconfigured builtin is deliberately preserved (surfaces
a clear runtime auth error rather than a silent swap; a cheap env/config credential
check would mis-classify OAuth/auth-store providers). Keeps the #3867 cached-catalog
hot path intact.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-10 22:16:25 +00:00
nesquena-hermes
6bac70d298 fix: preserve live stream output across session switches (cross-client)
Adds a server-side run-journal live snapshot (_run_journal_live_snapshot) returned
in GET /api/session as runtime_journal_snapshot, so a FRESH client (another device,
or a tab with no in-memory snapshot) opening an in-progress session immediately sees
the already-streamed assistant text + tool cards rebuilt from the server. Composes
with the existing _replay_run_journal cursor path (seeds lastRunJournalSeq so replay
resumes from the snapshot cutoff, not duplicating it) and keys tool cards by the same
5 id aliases (tid/id/tool_call_id/tool_use_id/call_id) as #3763 so SSE replay replaces
rather than duplicates snapshot cards. Payload values truncated; redaction test added.

Co-authored-by: t3chn0pr13st <technopriest@live.ru>
2026-06-10 20:12:05 +00:00
nesquena-hermes
6be19804f5 fix(routes): bound session/move lock + safe project-delete unlink during streaming (#3746) (#3922)
Two distinct timeout causes, both surfacing as the client's 30s 'Request timed
out' toast with no server-side signal:

A) /api/session/move acquired the per-session agent lock with a bare unbounded
   'with _get_session_agent_lock(sid):'. The streaming thread holds that same
   lock during checkpoint saves; on slow file I/O (WSL/DrvFs) the move could
   block past the client abort. Now acquires with timeout=5 and returns HTTP 503
   on contention (lock kept, not dropped, since s.save() still races the writer).

B) /api/projects/delete unlinked every assigned session via get_session()+save()
   — O(N) full-messages reserialize. For an actively-streaming session we now
   clear project_id on the LIVE CACHED Session object under LOCK (the streaming
   thread persists it on its next save — the worker always does a final save at
   turn completion) instead of issuing a competing s.save(); falls back to a
   direct save when not cached. Non-streaming sessions unchanged.

Also guards the '+ New project and move' shortcut (sessions.js) against the new
503 so it shows a toast instead of an unhandled rejection, keeping the #2551
authoritative refetch in both the success and catch paths.

Adds tests/test_issue3746_session_move_delete_timeout.py (behavioral lock-timeout
test + structural guards for both handlers + the frontend 503 guard). Widened the
#2551 new-project-refetch test's fixed byte-window to a block-scoped search so the
try/catch wrap (which preserves the refetch) doesn't trip a brittle offset assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-10 02:53:01 -07:00
nesquena-hermes
cc7f8a274a Release v0.51.345 — Release LI (model override picker for scheduled jobs #3809) (#3896)
Some checks failed
Release & Docker / release (push) Has been cancelled
Absorbs contributor PR #3809 (@b3nw), rebased onto fresh master (was ~20 behind,
panels.js conflict resolved by merging the new !isNoAgent skill-tags guard with
the model-select call).

Adds a Model Override dropdown to the Tasks scheduled-jobs create/edit form,
populated from /api/models grouped by provider, persisting model+provider,
clearable to default, disabled in no-agent mode. Surfaces hermes-agent's existing
per-job model override (CLI parity).

greptile P1s (override cleared on fast-save / on API failure) verified
ALREADY-FIXED in PR head; also applied an Opus UX hardening (keep the model
select disabled on a failed /api/models load so the user can't think they
cleared the override). UX approved by Nathan via screenshots.

Pre-merge fixes:
- i18n: the PR added the 3 cron_model_* keys to all locales but left 10 of them
  as 'TODO: translate' English stubs (only es was done), tripping
  test_zh_hant_locale. Provided real translations for de/zh/zh-Hant/ru/ja/fr/pl/
  it/pt/tr.
- test isolation: #3809's new test file shifts pytest-shard composition so
  test_issue2863's background-rebuild test ran after a test that leaves the
  #3884 _SESSION_INDEX_REBUILD_THREAD globals populated, suppressing the fresh
  thread it asserts on. Made that test hermetic (joins+clears the rebuild-thread
  globals up front) so it passes regardless of shard run order.

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-09 14:45:59 -07:00
nesquena-hermes
057307b3f9 fix(session): retire stale truncation watermark on new committed turn (#3831) (#3890)
* fix(session): retire stale truncation watermark on new committed turn (#3831)

retry_last / undo_last / the Edit-truncate handler set truncation_watermark
to suppress the *replaced* tail from the append-only state.db merge.
Session.save() deliberately never auto-clears it (#2914), but nothing retired
it when the user then sent a genuinely NEW turn either — so it froze at the old
edit boundary. A frozen watermark then dropped post-watermark state.db rows
whenever the sidecar was later reconstructed empty (recovery/reconcile),
permanently losing the turns sent after the edit (state.db still had them).

Retire a POSITIVE watermark to None once the new user turn is COMMITTED to
session.messages — at the success-merge (3 sites), eager-checkpoint, error/
recovery materialization, and cold-load repair commit points. Not at chat-start:
in deferred mode the new row isn't in messages yet, so a merge in that window
would resurrect the replaced tail (the max-sidecar guard hasn't risen past the
old boundary). Once committed, max_sidecar_timestamp rises past the replaced
tail and the merge suppresses it without the watermark, so retiring is safe.

Cleared to None, never 0.0 — 0.0 is the truncate-to-empty sentinel (#2914) that
must keep blocking all state replay, so the clear is falsy-gated.

Closes #3831

* chore(changelog): clarify watermark-retirement timing to commit-time

Greptile review noted the original phrase "retires the watermark at the
start of a new user turn" was timing-imprecise. The retirement actually
fires when the new turn is durably committed to session.messages —
at the agent-result merge, the eager user-message checkpoint, or the
cold-load recovery commit. Reword for accuracy; semantics unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(#3831): add regression tests for the two inline watermark-clear paths (greptile P2)

Cover the error/cancel materialization path (_materialize_pending_user_turn_before_error)
and the eager first-turn checkpoint path (_checkpoint_user_message_for_eager_session_save),
which inline the falsy-gated watermark clear instead of calling the tested helper.
The error path is precisely the #3831 failure mode (recovery/reconcile after a
crash), so a dedicated regression test closes that gap. Both assert a positive
watermark clears to None while the 0.0 truncate-to-empty sentinel (#2914) is
preserved.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 12:39:32 -07:00
nesquena-hermes
26e133e3e8 [HELD — independent review pending] Release v0.51.340 — bg_task agent wakeup (trio #2968+#2971+#2979) (#3867)
Some checks failed
Release & Docker / release (push) Has been cancelled
* stage bg_task trio combined (#2979 superset) on master for deep review

* fix(bg_task): unsubscribe SessionChannel on header-write failure (Codex deep-review catch) + regression test

* test: realign on-subscribe-recovery anchor to subscribe_to_session_channel after leak fix

* CHANGELOG: bg_task trio as v0.51.340 LD (HELD pending independent review)

* bg_task trio: apply 3 independent-review (greptile) fixes

1. start_session_turn now threads the session PROFILE model defaults
   (_read_profile_model_config) into the wakeup model-resolve, so a brand-new
   session with an empty model falls back to the profile default not global
   DEFAULT_MODEL. Updated the white-box spy test signature accordingly.
2. /api/session/stream omits the Connection header (HTTP/1.1 keep-alive
   default) to match the #3103 long-lived-SSE pattern.
3. Reaper now prunes _LAST_EMIT_TS for collected sessions so the coalesce
   timestamp map can't grow one permanent entry per session forever.

nesquena APPROVED the PR; these are the 3 non-blocking greptile suggestions.

* test: realign _start_session_turn adapter stub lambda to new profile-defaults signature
2026-06-08 22:36:18 -07:00
nesquena-hermes
8d4ed1f90d Release v0.51.338 — Release LB (saved prompts library, #3571) (#3860)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(composer): add saved prompts library with per-profile storage (#2732)

* fix(composer): move saved-prompts popup out of .composer-left to preserve DOM test (#2732)

* fix(composer): correct ARIA roles, add server-side prompt limits (#2732)

* fix(composer): surface save-prompt errors instead of silent success toast (#2732)

* Release v0.51.338 — Release LB (saved prompts library, #3571)

Composer saved-prompts library (@rodboev): bookmark button → popup of saved
prompts; click to insert, save current input, delete. Persists to
$HERMES_HOME/webui/saved_prompts.json with server-side caps (8000 chars / 200).

Maintainer work (per Nathan): conditions were (a) verify it actually works and
(b) hide on mobile. Both met:
- Live-verified load/save/delete all persist through the UI.
- Added mobile-hide (#btnSavedPrompts,.saved-prompts-popup display:none in the
  @media max-width:640px + 900px composer blocks). DOM-verified visible at 1280px,
  vision-confirmed absent from the composer at 390px.
- Added missing Polish (pl) i18n for the 5 saved_prompts_* keys (PR had en+others
  but not pl — failed locale-parity).
- Added tests/test_issue3571_saved_prompts.py (mobile-hide + caps + wiring guards).

Full suite green, ESLint/scope-undef CLEAN, Opus SHIP-safe (auth-gated, CSRF,
XSS-safe, sane caps), Codex SAFE-TO-SHIP.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-08 19:32:12 -07:00
nesquena-hermes
7674bfb449 Release v0.51.328 — Release KR (#3800 full compaction summaries + #3061 pending-intent RFC) (#3839)
Some checks failed
Release & Docker / release (push) Has been cancelled
#3835 (#3800): preserve full compaction summaries. #3061: pending-intent RFC (docs). Full suite 8273, Codex SAFE, Opus SHIP, CI 11/11. #3837 dropped (red CI). Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-08 11:35:39 -07:00
nesquena-hermes
b96f0f6d6f Release v0.51.327 — Release KQ (brick wave: #3829 + #3828 + #3822) (#3836)
Some checks failed
Release & Docker / release (push) Has been cancelled
Brick wave: stale-cache freshness (#3829), compression-tail preservation (#3828), interrupt-successor race (#3822, +bounded-guard hardening). Full suite 8271, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: dso2ng <dso2ng@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-08 11:06:59 -07:00
nesquena-hermes
396d0d0abd Release v0.51.326 — Release KP (#3618 + #3802 + #3762 + #3810) (#3816)
Some checks failed
Release & Docker / release (push) Has been cancelled
Batch: mic STT capability probe+fallback (#3618, live-drive verified), journal cleanup on delete (#3802), minimal-schema SQL guard (#3762), Help hover readability (#3810). Full suite 8265, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
2026-06-07 23:18:51 -07:00
nesquena-hermes
9d94298278 Release v0.51.321 — Release KK (Phase 3 light: load renderable transcript tails, #3790) (#3798)
Some checks failed
Release & Docker / release (push) Has been cancelled
Phase-3-light. #3790 (@ai-ag2026): expand cold-load transcript window to ~msg_limit renderable rows so tool-heavy sessions don't open showing 1-2 messages. Codex CORE fix: explicit expand_renderable flag (cold-load only; Load-earlier keeps raw cap). Also fixed a recurring CI timing flake (git-parallel test → deterministic Barrier). Full suite 8228, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-06-07 15:22:44 -07:00
nesquena-hermes
59de540b3d Release v0.51.320 — Release KJ (Phase 2: Polish (pl) language support, #3781) (#3796)
Some checks failed
Release & Docker / release (push) Has been cancelled
Complete Polish locale. #3781 (@leszek3737). Full suite green, CI 11/11, 180 locale/parity tests pass. Co-authored-by: leszek3737 <leszek3737@users.noreply.github.com>
2026-06-07 13:48:52 -07:00
nesquena-hermes
938ac9f55b Release v0.51.311 — Release KA (brick-wave: workspace Git RCE hardening #3769 + stale-snapshot sidebar visibility #3770) (#3776)
Some checks failed
Release & Docker / release (push) Has been cancelled
Brick-wave batch. #3769 (@Hinotoi-agent) hardens workspace Git config execution against repo-local RCE; #3770 (@ai-ag2026) keeps fuller pre-compression snapshots visible when _index.json is stale. Full suite 8176 passed, Codex SAFE, Opus SHIP IT. Co-authored-by: Hinotoi-agent; Co-authored-by: ai-ag2026
2026-06-07 08:05:30 -07:00
nesquena-hermes
b6431eb677 Release v0.51.308 — Release JX (#3765 — gate onboarding-complete like its siblings) (#3765)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(security): gate /api/onboarding/complete on the local-network check (#3765)

Sibling-path gap surfaced by the #3758 release gate. /api/onboarding/oauth/start,
/setup, and /probe are gated by _onboarding_gate_allows(), but
/api/onboarding/complete was not — it called complete_onboarding() unconditionally
(persists onboarding_completed=True, which hides the first-run wizard). On a
passwordless public bind, an unauthenticated no-Origin POST passes generic CSRF
and could flip the wizard off.

Pre-existing (the endpoint was ungated before #3758 too; #3758 only refactored the
three already-gated siblings). Low severity — it toggles a UI flag, not credentials
or access — but the inconsistency is a real hole, so close it the same way as its
siblings.

- Gate /api/onboarding/complete with _onboarding_gate_allows() → 403 when denied.
- Regression tests: public client (no forwarded headers) → 403 + complete_onboarding
  NOT called; loopback client → 200; auth-enabled → 200.
- Mark the legacy _is_local_from_handler mirror in test_onboarding_network.py as a
  STALE pre-#3758 contract (it trusts unauthenticated XFF); the authoritative
  trust-matrix tests live in test_security_review_fixes.py. Migrating the mirror to
  delegate to the real helper is tracked as follow-up test debt, out of scope here.

* docs(changelog): stamp v0.51.308 — Release JX (#3765 onboarding-complete sibling-consistency gate)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 20:42:23 -07:00
nesquena-hermes
70596e6993 Release v0.51.307 — Release JW (stage-a3 — onboarding spoof fix + update-check CSRF, #3758 partial) (#3764)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(security): ignore spoofable forwarded IPs in onboarding gate + make update-check CSRF-safe (#3758, partial)

Ships the two unambiguous slices of #3758's security review. The two slices with
breakage risk for existing installs — the Docker-default public-bind-requires-auth
gate and removing /tmp from the /api/media allowed roots — are held for separate
review/decision.

Onboarding forwarded-IP spoof hardening (+ release-gate CORE fix):
- The unauthenticated first-run onboarding local-network gate now IGNORES
  X-Forwarded-For / X-Real-IP by default (a direct client can spoof them to a
  private/loopback address to bypass the gate), trusting them only when
  HERMES_WEBUI_TRUST_FORWARDED_FOR=1 is set behind a trusted proxy (rightmost
  proxy-appended hop).
- Release-gate (Codex) CORE catch + refinement: when forwarded headers are
  present but untrusted, the header is ignored and locality is judged by the raw
  socket — but a PRIVATE/LAN raw socket (a separate proxy box that could forward
  an arbitrary public client) is no longer treated as local; only a LOOPBACK raw
  socket is (genuine same-host; a remote attacker can't forge a 127.0.0.1 TCP
  source). This closes the new fail-open the initial refactor introduced (public
  client behind a LAN proxy read as local) while preserving genuine same-host
  onboarding. LAN-proxy operators must set HERMES_WEBUI_TRUST_FORWARDED_FOR=1.
  Regression tests lock the full matrix (spoof-block, LAN-proxy-deny,
  loopback-allow, trusted-proxy-rightmost-hop, direct-public-deny).
- Three duplicated inline gate blocks unified into _onboarding_gate_allows /
  _onboarding_request_is_local; ONBOARDING_OPEN normalized to canonical truthy
  values via _truthy_env.

Update-check CSRF hardening:
- GET /api/updates/check is cache-only (cached_update_status(): no network/git
  mutation); forced refresh moves to POST /api/updates/check {force:true}; both
  frontend call sites updated and the test_api_timeout contract assertion updated.
- cached_update_status() preserves cached agent info when include_agent re-enabled.

Docker log masking: ENV_OBFUSCATE_PART also masks PASSWORD/SECRET/CREDENTIAL/COOKIE/SESSION.

Held for separate review (NOT in this PR): public-bind-requires-auth startup gate
(server.py + Dockerfile default) and the /api/media /tmp-root removal.

Co-authored-by: fantasticsquirrel <[email protected]>

* docs(changelog): stamp v0.51.307 — Release JW (stage-a3 #3758 partial)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 19:50:56 -07:00
nesquena-hermes
1cb9d3b550 fix(#3717): context-length indicator honors provider per-model overrides (#3726) (#3752)
Some checks failed
Release & Docker / release (push) Has been cancelled
@rodboev. providers.<name>.models.<model>.context_length overrides (standard provider,
no base_url) were invisible to the session context resolver → wrong window shown/persisted,
could trip auto-compression at the wrong threshold. New _context_length_lookup_inputs_for_model
helper resolves provider config / base_url / custom_providers across route-load, session-save,
and SSE-usage paths; provider-scoped overrides match by provider name and forward as
config_context_length (returned before any base-url-gated probe).

Maintainer pre-merge items both already satisfied in PR head: no-base_url regression test
(test_route_resolver_uses_provider_model_context_length_without_base_url) present; session-save
_cfg_base_url assigned before the helper call (safe-bound, no NameError on TypeError fallback).
Verified api code byte-identical to PR head; 14 context-length tests pass. + CHANGELOG v0.51.300.

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 15:57:09 -07:00
nesquena-hermes
ce4c2c1de1 Release v0.51.298 — stage-3719 (live model probe for custom providers with model config #3719) (#3747)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(#3718): /api/models/live probes upstream for custom providers with model config (#3719)

@DanielMaly. Config model IDs were added to the ids list before the 'if not ids:' guard,
so a custom provider with a model: field skipped the live /v1/models probe and Settings'
refresh returned only the config entry. Now collects config IDs separately, always probes
for custom providers, merges live (priority) + config (fallback). Includes the maintainer
review follow-ups (CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant + behavioral tests).

Captured all 3 logical PR commits' net effect; routes.py + test verified byte-identical
to the PR head. + CHANGELOG v0.51.298.

* test(#3718): remove unused BytesIO import (ruff F401)

* test(#3719): update timeout assertion to CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS

The #3719 maintainer-review commit replaced the hardcoded urlopen timeout=8 with the
CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant (5.0). test_named_custom_live_fetch_uses_matching_entry_endpoint
asserted the old literal 8. Reference the constant directly now so the assertion can't
drift again. Not a behavior change — only the live-probe timeout value (8s -> 5s) moved,
URL + auth unchanged.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 14:48:39 -07:00
nesquena-hermes
4f842d5095 Release v0.51.297 — stage-3711 (terminal remote-backend guard #3711) (#3745)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(terminal): guard embedded terminal on remote backends (#3673)

* fix(terminal): add missing remote-backend locale key

* fix(terminal): add missing remote-backend locale coverage (#3673)

* docs(changelog): v0.51.297 — terminal remote-backend guard (#3711) only

Dropped #3725 (descendant reaper) from this stage: Codex caught a SILENT exit-code
clobber — its process-wide os.waitpid(-1, WNOHANG) can reap a sibling WebUI child that
another subsystem is waiting on, coercing that child's returncode to 0 (failures become
successes). Held for the contributor to scope the reaper to terminal PGIDs
(os.waitpid(-term.proc.pid, WNOHANG)) or a terminal-PGID registry.

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 14:17:17 -07:00
nesquena-hermes
65c4bc9fa2 Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) (#3743)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix: honor explicit model pick, suppress silent revert on cross-family selection (#3737)

When a user changes the model in the composer dropdown and sends,
_resolve_compatible_session_model_state previously had no way to
distinguish an explicit user pick from stale session state. The
profile-aware branch (v0.51.290, PR #3448) and the legacy block
both rewrote bare cross-family models to the profile default, and
the client unconditionally applied effective_model — silently
discarding the user's choice.

Backend: accept explicit_model_pick flag (default False) on
_resolve_compatible_session_model_state. Guard both the
profile-aware branch (routes.py:2024) and the legacy block
(routes.py:2124) to skip cross-provider normalization when set.
_handle_chat_start extracts the flag and passes it through.

Frontend: consult _readPendingSessionModel (sessionStorage, 10-min
window) to detect explicit picks and include the flag. Add a toast
as defense-in-depth when the server still returns effective_model.

Closes #3737

* fix: tighten explicit-pick detection and add regression tests (#3737)

Greptile P2-1: compare model_provider in pending pick detection,
not just model name, to avoid false-positive flag when the
session provider changes between pick and send.

Greptile P2-2: only show the defense-in-depth toast when an
explicit pick was actually overridden — stale-session
normalizations are expected behavior and should be silent.

Add two regression tests for the profile-branch guard:
- explicit_model_pick=True → cross-family model survives
- explicit_model_pick=False → existing normalization preserved

* revert(sidebar): remove manual session status labels (#3570)

The manual per-session status labels (Todo / In Progress / Done) added in
v0.51.284 (#3570) stored state only in browser localStorage keyed by session
id, with no server-side backing — so labels did not persist across browsers
or devices (a user who labeled sessions on one machine saw none after moving
to a laptop). They also rendered as three flat top-level entries in the
session context menu, crowding the root menu.

Per maintainer decision, remove the feature entirely for now. It can be
reintroduced later with proper server-side persistence and a less intrusive
menu treatment.

Removes:
- JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js)
- context-menu status entries + sidebar status badge render
- .session-manual-status* CSS (static/style.css)
- session_status_* locale strings across all locales (static/i18n.js)

Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean.

reverts #3570

* fix(#3737): keep explicit-pick marker until send consumes it (Codex catch)

Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js
modelSelect.onchange cleared the pending-pick marker right after /api/session/update,
so by the time send() ran _readPendingSessionModel returned null, _explicitPick was
false, and the server's profile-provider branch still reverted the cross-family pick
(the exact #3737 bug). The flag only worked in the rare race where send beat the
session-update round-trip.

Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send()
immediately after reading a matching pending pick, so it's consumed for that send only.
onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature
clear is removed.

* test(#3737): lock client clear-timing wiring (onchange records, send consumes)

Static source guards for the Codex clear-timing fix: onchange must record the
pending pick and NOT clear it post-session-update; send() must consume (clear) it
only after reading a matching _explicitPick, and send the flag only when truthy.
Complements the author's resolver-level tests in test_provider_mismatch.py.

* test(#3737): realign refresh-persistence test to the moved pending-pick clear

The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange
into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update
asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body).
Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to
send). The test's core refresh-survives invariant (marker recorded before the async
session-update; reapplied on load) is unchanged and still passes; only the stale
clear-location assertion is flipped. Not a regression-blessing: the refresh-survives
feature is intact, the marker lifecycle is more correct.

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 13:39:14 -07:00
nesquena-hermes
e3a7c93dc6 [HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741)
Some checks failed
Release & Docker / release (push) Has been cancelled
* Harden interrupted recovery control filtering

* Redesign live-to-final assistant replies

* Fix live activity anchor test fixture

* Fix CI lint issues for live reply tests

* Strengthen live progress prompt contract

* Recover PR #3401 refresh on origin/master

* Repair live-to-final refresh regressions

* Fix live worklog refresh regressions

* Show live footer timer on initial stream start

* Restore live stream shell after reload

* Preserve per-frame live SSE replay cursors

* Preserve reasoning as Worklog Thinking cards

* Quiet Worklog Thinking card styling

* Align Worklog Thinking card styling

* Scope live Worklog Thinking cards by segment

* Suppress exact duplicate settled Thinking

* Close #3401 merge review test gaps

* fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog)

Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor
surfaced 4 regressions vs master that the rewritten suite no longer guarded:

1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText
   used $-anchored regexes so a leading think block + visible answer extracted nothing
   and the Thinking card never rendered. Removed the 3 $ anchors to match the
   (non-anchored) display stripper. Live: inline-think thinking-only turn now renders.
2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced
   a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload
   replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the
   cursor onto INFLIGHT + schedules a throttled persist.
3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in
   the picker. Restored the neon CSS block from master.
4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled
   turn's worklog when renderMessages re-ran during an active stream (switch-back to an
   in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length).
   Live: busy re-render now preserves tool cards (4→4, was 4→0).

Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none
below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7).

* test(#3401): realign 3 stale source-shape assertions to the deep-review fixes

Fix commit changed two source literals that existing stage tests scanned for:
- test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored
  'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4).
- test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment
  contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'.
Intent of all three assertions unchanged; only the matched string updated. No code
behavior change.

* docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 12:12:37 -07:00
nesquena-hermes
47020925d5 Release v0.51.290 — Release JF (stage-s1 — profile provider/model resolution #3448 fixes #3405) (#3703)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(#3405): respect profile provider/model in session resolution (#3448)

Profile-bound sessions now resolve their provider/model from the profile
instead of silently falling back to the global active provider — fixes wrong
credentials/billing and silent context truncation. Repairs stale models under
the profile provider (incl. the openai-codex + openai/ slash-model case) while
preserving native slash IDs on openrouter/custom.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.290 — Release JF (stage-s1, #3448 fixes #3405)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:17:27 -07:00
nesquena-hermes
e9bb354908 Release v0.51.285 — Release JA (stage-r19 — update-reload server-identity race fix #3654) (#3693)
Some checks failed
Release & Docker / release (push) Has been cancelled
* Fix update reload readiness race — poll /health server identity before reload (#3654)

Replaces the raw-uptime comparison (couldn't distinguish a fresh old process
from the restarted one) with a stable server_started_at identity read before
the update POST; reloads only when the identity changes. Both the force-update
and regular apply paths read + pass the baseline. (#874, #3654)

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.285 — Release JA (stage-r19)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:13:35 -07:00
nesquena-hermes
988348682c Release v0.51.284 — Release IZ (stage-w4 — sidebar status labels + cron-sessions toggle #3570 #3514) (#3692)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* feat(sidebar): add manual session status labels (#3570)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.284 — Release IZ (stage-w4)

* fix(settings): persist show_cron_sessions in the explicit Save Settings path too (#3514)

Codex regression-gate follow-up: the autosave path (_preferencesPayloadFromUi)
included show_cron_sessions but the explicit saveSettings() button path read/saved
show_cli_sessions and dropped the cron checkbox — clicking Save Settings silently
omitted it. Read settingsShowCronSessions + add body.show_cron_sessions (gated on
CLI sessions, mirroring autosave).

* fix(settings): gate show_cron_sessions identically in BOTH save paths (#3514)

Codex round-2: my saveSettings() gate exposed that the autosave path
(_preferencesPayloadFromUi) posted the raw cron checkbox state ungated, so
show_cli_sessions=false + show_cron_sessions=true could persist via autosave.
Gate autosave on showCliCb too; update the regression test to assert both
paths gate on settingsShowCliSessions.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 17:48:27 -07:00
nesquena-hermes
b5caf83ff9 Release v0.51.279 — Release IU (stage-p3h — preserve Activity/streaming turn on mid-stream scroll #3665) (#3686)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (#3665, #3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
2026-06-05 14:45:31 -07:00
nesquena-hermes
87084dfebf Release v0.51.276 — Release IR (stage-p3e — preserve manually-named session titles #3542) (#3682)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(sessions): skip adaptive auto-rename for manually-named sessions (#3542, #3230)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* docs(changelog): v0.51.276 — Release IR (stage-p3e)

* fix(sessions): clear manual_title lock on /api/session/clear (#3542)

Codex regression-gate follow-up: the clear endpoint reset the title to
Untitled directly, stranding manual_title=True so the reused session never
auto-named again. Route the reset through apply_session_title_rename (which
clears the lock for auto-labels) + add a behavioral and a static-guard test.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-05 13:34:54 -07:00
nesquena-hermes
165454dd7b Release v0.51.275 — Release IQ (stage-p3d — approval SSE state extraction #3575) (#3681)
Some checks failed
Release & Docker / release (push) Has been cancelled
* refactor(routes): extract approval SSE state into api/route_approvals.py (#3575)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.275 — Release IQ (stage-p3d)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 13:10:45 -07:00
nesquena-hermes
9c6a96f483 Release v0.51.274 — Release IP (stage-p3c — symlink-swap TOCTOU hardening #3630) (#3680)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(security): harden routes file APIs against symlink swaps (#3630, #3450)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.274 — Release IP (stage-p3c)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 12:58:11 -07:00
nesquena-hermes
9d605c41e2 Release v0.51.273 — Release IO (stage-p3b — cron-output traversal guard #3661) (#3679)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(security): reject traversal-shaped job_id in cron output endpoint (#3661)

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>

* docs(changelog): v0.51.273 — Release IO (stage-p3b)

* test(cron): guard new cron-output tests with @requires_agent_modules (#3661)

The two new direct-handler tests import cron.jobs, which lives in hermes-agent
and is NOT installed in CI — without the marker they error/hang in the no-agent
CI shard (caught by the shard-0 timeout). Mirrors how the other 30 agent-dependent
tests skip cleanly when hermes-agent modules aren't importable.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
2026-06-05 12:45:47 -07:00
nesquena-hermes
58528a4d88 Release v0.51.270 — Release IL (stage-u1 — un-hold batch: #3517 #3624 #3613) (#3674)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(commands): add /use to force a skill for the next turn (#3517, #2977)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(auth): cap pending passkey challenges by evicting oldest, not rejecting (#3624)

Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>

* fix(providers): expose model-provider plugins in WebUI (#3613)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* docs(changelog): v0.51.270 — Release IL (stage-u1, 3-PR un-hold batch; #3448 + #3618 dropped)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-06-05 11:35:26 -07:00
nesquena-hermes
2c7b530071 Release v0.51.269 — Release IK (stage-b2 — sidebar perf + search scope + Windows ctl) (#3672)
Some checks failed
Release & Docker / release (push) Has been cancelled
* perf(ui): single-pass sidebar session row partitioning (#3658)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* fix(search): scope session search to active profile (#3646)

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>

* fix(ctl): tree-kill ctl.sh stop on Windows (#3670)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.269 — Release IK (stage-b2)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 10:53:45 -07:00
nesquena-hermes
442b033e67 Release v0.51.268 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes) (#3671)
Some checks failed
Release & Docker / release (push) Has been cancelled
* perf(providers): O(1) codex cache merge membership checks (#3656)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650)

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>

* fix(clarify): don't stash clarify draft while submission is in flight (#3651)

Co-authored-by: carryzuo00 <carryzuo00@gmail.com>

* perf(sessions): batch lineage report child fetch by parent id (#3659)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* perf(sessions): batch orphan sidecar state.db existence probes (#3657)

Co-authored-by: Pamnard <pamnard@users.noreply.github.com>

* test(streaming): pin DOM-INFLIGHT reattach invariant (#3572)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.268 — Release IJ (stage-b1)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
2026-06-05 10:44:02 -07:00
nesquena-hermes
f1211e1f0c Release v0.51.267 — Release II (stage-r17) (#3648)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.267 — Release II (stage-r17)

Security hardening cluster — 3 @zapabob PRs (forwarded-header trust + TTS prosody validation).

### Security
| Issue/PR | Author | Hardening |
|----------|--------|-----------|
| #3640 | @zapabob | `/api/tts` per-client throttle no longer trusts `X-Forwarded-For` by default (can't spoof to evade the rate limit); forwarded IP honored only behind a trusted-proxy opt-in. |
| #3642 | @zapabob | CSRF same-origin check no longer trusts `X-Forwarded-Host`/`X-Real-Host` by default (closes a forwarded-host CSRF bypass); opt-in keeps legit reverse-proxy deploys working; default uses the real `Host`. |
| #3643 | @zapabob | Browser-provided TTS prosody (rate/pitch/volume) validated against the `±N%` / `±NHz` grammar before `edge_tts.Communicate`. |

### Attribution
Each contributor branch was **rebased onto current master and pushed back to @zapabob's fork** (native authorship preserved), so the source PRs are current/mergeable. Shipped here as one release because all three add a `[Unreleased]` CHANGELOG entry at the same location (merging individually would force a rebase-cascade). Source PRs #3640/#3642/#3643 closed as merged-via-release with credit.

### Gate
- Full pytest suite: **7779 passed, 0 failed**
- ruff: CLEAN
- revert-guard: PASS (all 3 branches rebased; master is an ancestor)
- Codex (regression): **SAFE TO SHIP** — each hardening is **default-secure AND opt-in-compatible** (no legit reverse-proxy/tunnel deploy breaks on update): CSRF forwarded-host default-off + opt-in works + normal same-origin still passes; TTS prosody rejects out-of-grammar input, legit `+N%` passes; TTS throttle ignores spoofed XFF by default.

Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
2026-06-05 01:19:59 -07:00