* stage #4612 (santastabber): surface loopback sidecar diagnostics + CHANGELOG
Settings -> Extensions surfaces manifest-declared loopback sidecar companion services
(type:loopback, http(s) on 127.0.0.1/localhost/::1 only, sanitized origin+health_path)
as read-only health cards w/ a browser-side credentials-omitted health probe. Backend
hard-whitelists scheme + loopback host, rejects userinfo/path/query/traversal, rebuilds
output from parsed components (no echo of rejected input); frontend esc()'s every field,
whitelists badge status, fetch never reads body. Purely additive to GET /api/extensions/status.
Code applied byte-faithful from PR head 7080929f37. 33 own tests pass.
Nathan concept-approved (loopback-companion direction).
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* fix#4612 Codex gate findings: reject encoded query/fragment in health_path + align CSP loopback allowlist
Codex SHIP-WITH-FIXES (2 reproduced SILENT findings):
1. _normalize_sidecar_health_path percent-decoded AFTER the raw query/fragment ban,
so /health%3Ftoken=abc -> ?token=abc survived into the probed URL. Now re-rejects
decoded ? and # + regression test (%3F/%23 skipped with sidecar_health_path_rejected).
2. The origin validator accepts https:// and [::1], but CSP connect-src only listed
http 127.0.0.1/localhost -> accepted https/IPv6 loopback sidecars would be CSP-blocked.
Aligned _CSP_CONNECT_BASE (added https v4/localhost + http(s) [::1]) + updated the 4
exact-string CSP tests + a regression test asserting all loopback forms are allowed.
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* Release v0.51.564 — Release TW (loopback sidecar diagnostics; #4612)
* fix(#4612): drop browser-invalid [::1]:* CSP source (browser-smoke fail)
The CSP-loopback-alignment fix added http(s)://[::1]:* to connect-src, but CSP
host-source grammar can't express a port wildcard on a bracketed IPv6 literal —
Chromium rejects '[::1]:*' as an invalid source, which browser-smoke flags as a
console error (CI fail). Keep the valid https://127.0.0.1:*/localhost:* additions;
drop the IPv6 wildcard entries. IPv6 [::1] sidecars are still accepted by the
validator (displayed in the card); their browser health probe surfaces as
'blocked' under CSP unless an operator adds a specific-port [::1]:<port> via
HERMES_WEBUI_CSP_CONNECT_EXTRA. Tests updated to match + assert [::1]:* absent.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* stage #4380 (santastabber): render selected context as quote cards + CHANGELOG
Evolves the shipped #2543 named-context-blocks: composer chips -> quote cards
(accent rail, editable label w/ rename, remove, clipped excerpt) + thread render
of labeled selections as <figure> cards. Label+quote esc()'d on every path
(adversarial harness 0 survivors). 3 new i18n keys in all 13 locales (parity green).
Code applied from PR head 2eefc11c02 (own 3-dot contribution, CHANGELOG re-stamped).
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* fix#4380 Codex gate finding: selection-only reply must enable the primary Send button
Codex CORE: #4380 moved selected context out of the textarea into _pendingSelections,
but _composerHasContent() only checked textarea+files -> getComposerPrimaryAction()
returned disabled for a selection-only reply (desktop Enter calls send() directly so
it worked, but click/tap/mobile users were blocked). Fix: expose
window._hasPendingSelections() from messages.js, fold it into _composerHasContent(),
and call updateSendBtn() from _renderSelectionChips() so add/remove/clear refreshes
the button. + regression test.
* Release v0.51.562 — Release TU (selected-context quote cards; #4380)
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
The repo enforces every locale defines every English key (test_*_locale.py). Adding the key to
English only broke 6 locale-coverage tests. Added the key (English fallback text) to all 13 locale
blocks. Full suite catches what targeted tests miss.
Codex gate (SILENT): panels.js referenced t('cron_model_no_agent_hint') but the key was never added
to i18n.js. t() returns the key name for a missing translation (falls back to LOCALES.en, then the
key), so script-only cron jobs rendered the literal 'cron_model_no_agent_hint'. Added the English key
(other locales fall back to en) + strengthened the i18n-keys test to assert the value is defined.
Codex gate (SILENT): the new outside-symlink info dialog uses showConfirmDialog({hideCancel:true})
which sets the shared #appDialogCancel to display:none; showPromptDialog only reset textContent, so a
later rename/new-file prompt opened with no Cancel button. Fix: showPromptDialog now restores
cancelBtn.style.display='' first. + regression test.
Codex+Opus gate (info-leak): the display-only escape-target rows still returned the resolved
outside path (target), target-derived is_dir, and target stat size; ui.js showed the outside path
in the open-dialog. Fix: emit ONLY name/path/type/target_outside_workspace/mtime for outside rows
(no target, no size, is_dir=False); dialog uses a generic 'points outside workspace' message (dropped
{target} from external_link_open_confirm across all 13 locales). Containment was already intact (both
gates confirmed); this closes the path-disclosure. Tests updated to assert the no-leak property.
claw-io: symlinks whose target resolves outside the workspace root were silently dropped; now emitted
with target_outside_workspace=True (display-only — safe_resolve_ws/open_anchored_fd still block
navigation). Dedup-winner over #4573 (stronger tests). Co-authored-by: claw-io <claw-io@users.noreply.github.com>
latipun7: adds id-ID-GadisNeural to the Edge TTS allowlist (Edge TTS is a core builtin -> parity, default-in)
+ picker label + sets speaking-state at start of _playEdgeTtsChunked so the Listen button reflects playback.
Co-authored-by: latipun7 <latipun7@users.noreply.github.com>
Gate findings (Codex 2-CORE / Opus SHIP-WITH-FIXES), both fixed:
1. cross-profile leak — _pre_compression_continuation_session_id scanned all profiles' sessions,
matched only by parent_session_id; now filters children by _profiles_match(child, snapshot)
+ validates returned sid with is_safe_session_id. +cross-profile regression test.
2. restore-state poison — frontend wrote continuationSid to localStorage/URL BEFORE the inner
loadSession proved it loadable; removed the speculative write (success path at the loaded id
handles it). Updated the source-grep test to assert the safe form.
Bounded BFS (range(20)+seen) + recursion guard confirmed safe by both gates.
george-andra: GET /api/session surfaces continuation_session_id for a hidden pre-compression
snapshot; loadSession follows it so a mobile reload mid-compression recovers to the visible
continuation instead of a hidden snapshot (chat looked 'lost'). Resolved 2 conflicts (1615 behind):
sessions.js merged w/ #2971 re-arm guard; routes.py merged w/ _session_source_is_webui branch.
Co-authored-by: george-andra <george-andra@users.noreply.github.com>
franksong2702: preserve the reader's semantic viewport anchor during mid-stream transcript rebuilds
before raw scrollTop fallback; remount virtualized anchors before restore w/ _programmaticScroll
scoped to the remount frame; treat captured userUnpinned/pinned snapshot as authoritative so
distance-inference can't re-pin a reader who scrolled away during a live reply. Browser-only scroll
state. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Codex gate (SILENT): the scene-builder/hydrator _anchor_scene_row_looks_like_final_answer
(messages.js + routes.py) used a loose '>=80 + prefix' rule while the renderer
_anchorSceneProseMatchesFinalAnswer uses '>=80 + ratio>=0.9'. A short intermediate-prose row that
was a prefix of the final answer could be dropped from the persisted scene by the looser builder,
leaving the stricter renderer no row to show -> silent prose loss. Aligned both filters to the
renderer's rule (drop fewer -> preserve more intermediate prose). Matrix already GREEN; this closes
the edge the fixture didn't exercise.
Transparent Stream lost ALL tool/thinking activity on every rehydrate path (settle/switch/hide/blur/
reload) — only final answer survived, because _renderSettledAnchorSceneForMessage early-returned
outside Compact Worklog. Adds a transparent-mode settled renderer (same persisted activity_scene_v1,
event rows) + intermediate-prose preservation (suppress only the final-answer-dup prose). Compact
Worklog unchanged.
rodboev. Default-off; OFF path byte-identical to master (cache key 'u' + _renderUserFencedBlocks
unchanged when window._renderUserMarkdown falsy). ON routes user text through the same renderMd
SAFE_TAGS/_isSafeUrl sanitizer as assistant msgs. Nathan override-approved (likes the idea, opt-in).
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Codex ship-gate (CORE). ui.js:9241 re-exported three top-level functions via
same-name wrappers (window.X = function(){ return X() }). In a classic script a
top-level `function X(){}` IS window.X, so the wrapper reassigns that global to
itself → infinite recursion → RangeError: Maximum call stack size exceeded on
every live worklog render / session reattach / snapshot-restore path (send,
sessions.js:1264/1375/570). Replace with direct assignments. JS-only brick the
pytest suite can't catch (no JS execution) — confirmed by classic-script semantics
+ will verify live in-browser. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
- Remove the redundant 4th OR clause from _catalog_has_provider() — the
existing C1–C3 already handle compound provider IDs correctly (C2 IS
a normalized-vs-raw comparison; the 4th clause was a strict subset).
- Add && _activeProvider guard to _isCrossProviderPick in messages.js
to prevent spurious True when the profile has no configured default
provider (reviewer's optional hardening suggestion).
- Add test_catalog_has_provider_compound_ids.py verifying that compound
provider IDs like 'custom:glm-free-relay' are correctly recognized by
the existing C1–C3 clauses.
Two-part fix for the model selector jumping back to the profile default
after the first message when a cross-provider model (e.g. a custom relay
like custom:glm-free-relay) is selected.
Backend (api/routes.py):
- _catalog_has_provider() now matches compound provider IDs (e.g.
custom:zenmux-relay) whose normalized form (custom) is a raw
catalog provider_id. Previously the three existing checks missed
normalized-vs-raw comparison for compound hints, causing the catalog
lookup to fail and the fallback branch to silently revert the model
to the profile default on every non-explicit turn.
Frontend (static/messages.js):
- The explicit_model_pick marker was consumed after the first send(),
so subsequent sends lost the signal and the server repaired the
model back to the profile default (#3737 regression).
- Now, when the session has a non-default model from a different
provider than the profile's active provider, every send() sends
explicit_model_pick=true so the server honors the user's choice
across the entire conversation.
- The pending onchange marker is still consumed after the first send
(unchanged behavior); only the explicit_pick inference is broadened.
Codex SHIP-WITH-FIXES: non-done terminal paths (apperror/cancel/stream-error
finalize) didn't clear _STREAM_WAS_HIDDEN, so a stale wasHidden=true could be
inherited by a later same-sid reconnect -> spurious notification. Make each
entry stream-owned ({streamId, wasHidden}) so a stale entry from a prior stream
is never trusted (reconnect keeps state only when streamId matches; done reads
via the owned entry), and add _clearStreamHidden(activeSid, streamId) on the
apperror/cancel/finalize-fallback terminal paths (belt-and-suspenders).
A background tab's SSE is throttled by Chromium, so the stream 'done' event
arrives late (after the user returns, document.hidden=false) and the
response-complete notification was suppressed by the live visibility check.
Track per-stream whether the tab was hidden at ANY point during the stream
(_STREAM_WAS_HIDDEN, set at attach, OR'd by one idempotent visibilitychange
listener, read+cleared at done) and pass forceHidden accordingly.
sendBrowserNotification bypasses ONLY the live visibility gate on forceHidden,
still honoring the user's notifications-enabled setting — so a watched stream
stays silent (matches Slack/Discord/Gmail/Claude) but a backgrounded one
notifies. Correct fix for the #4416 root cause; supersedes #4421's always-fire
approach (which would also fire when the tab was never hidden).
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Gate-found (Opus SHIP-WITH-FIXES; Codex SAFE — split resolved by reading the
contract: no dropped-turn, but a real transient start-race flicker). #4424
removed the _sendInProgress guards entirely; that re-opened the #2689 start-race
SYMPTOM for the one session mid-send (a /api/sessions poll landing during the
/api/chat/start round-trip would blank the just-sent bubble + queue a spurious
force-reload). Re-add the guard SCOPED to sid===_sendInProgressSid only, in both
_reconcileActiveSessionIdleStateFromList and _purgeStaleInflightEntries — a
long-hung session has _sendInProgress===false so it still gets unstuck (#4354),
only the in-flight start window is protected. Also fixes Opus's test-masking
finding: test_inflight_send_start_race now strips JS comments before asserting +
requires a real , so a comment can't green-light the #2689 guard, and
adds coverage for the reconcile path.
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
The pre-session toolset comment pushed the #2518 provenance comment past the
test_issue2518 4000-char window assertion. Trim the comment to one line and
widen the test window to 5000 (the fallback block it actually guards is well
within range; newSession() has legitimately grown).
Gate-found: staged _pendingSessionToolsets could leak into a later New Chat
started from an existing session (newSession forwarded it whenever it was an
array; loadSession never cleared it). Gate the forward on !S.session and clear
the staged value when a real session loads. +2 regression tests.
Co-authored-by: claw-io <claw-io@users.noreply.github.com>