sync: upstream v0.51.184 (full catch-up from v0.51.131)#42
Merged
Conversation
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
11-PR low-risk cleanup: - nesquena#3043 openai-codex models.dev reasoning passes xhigh - nesquena#3044 reset _messagesTruncated on new session - nesquena#3047 discoverability: api lineage representative for stale CLI flag - nesquena#3049 title-language detection threshold + English false-positive fix - nesquena#3051 docker docs: sudo compose + Linux host-gateway - nesquena#3054 SSE reconnect: visible-but-unfocused current pane - nesquena#3055 fallback title: drop German-only Session Bilder case - nesquena#3056 title prompt: language-neutral instruction - nesquena#3070 /api/upload reports actual stored filename - nesquena#3071 clarify SSE fallback preserves owner session id - nesquena#3072 gateway-chat forwards image attachments as image_url parts
stage-batch35: v0.51.153 / Release DY — 11-PR low-risk cleanup
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md # tests/test_webui_gateway_chat_backend.py
# Conflicts: # CHANGELOG.md
9-PR medium-risk cleanup: - nesquena#3037 routes.py: argv-style prefill hook + env-var override for notes drawer - nesquena#3046 models.py: compression parent not repaired as stale interrupted turn - nesquena#3048 session_discoverability.py: --repair-safe CLI with default dry-run - nesquena#3053 ui.js: streaming KaTeX guard for parser-owned equations - nesquena#3059 models.py: empty partial activity rows excluded from sidebar recency - nesquena#3060 profiles.py: API key writes to .env (chmod 600), not config.yaml - nesquena#3064 routes.py: MEDIA: image tokens allow exact session-referenced paths - nesquena#3069 models.py: cron sessions with project_id surface via Cron Jobs chip - nesquena#3077 gateway_chat.py: HTTP 401 maps to gateway_auth_error event
…y-list (Codex review #2) Under a named profile, process HERMES_HOME is ~/.hermes/profiles/<name> but the allowlist still grants base ~/.hermes — so the prior deny (anchored only on the active-profile root + STATE_DIR) left ~/.hermes/state.db and sibling-profile secrets (~/.hermes/profiles/other/auth.json) reachable. Build deny roots from every Hermes state root the allowlist accepts: active HERMES_HOME, base ~/.hermes, api.profiles._DEFAULT_HERMES_HOME, and STATE_DIR; apply the state-subdir dir-denies under each. Widen the CSP-slice structural test window to match.
…kspace carve-out (Codex review #3) Codex round-3 found the prior multi-profile hardening OVER-blocked: denying STATE_DIR + base/profiles wholesale 403'd legitimate active-workspace media. Redesign around a single principle: the ACTIVE WORKSPACE is the user's own content (never deny), Hermes INTERNAL STATE lives outside any workspace (deny). If target is inside the active workspace -> allow; else deny known secret/config basenames + internal state subdirs across all Hermes roots. Also folds in Opus defense-in-depth: adds cron/logs/checkpoints/backups subdirs + gateway_state.json/channel_directory.json/jobs.json basenames. Adds an over-block regression test (a /tmp artifact named settings_* still serves 200).
…broad/internal roots (Codex review #4) Codex round-4: the carve-out could re-open the hole if the active workspace is pathologically set to a broad/internal root ($HOME, ~/.hermes, a profile root) — get_last_workspace only checks is_dir(), so workspace=~/.hermes would serve state.db. Gate the carve-out: disable it when the active workspace IS, CONTAINS, or is CONTAINED BY any Hermes root, or is $HOME / a */profiles dir / a named profile root / an internal state subdir. Adds a unit test proving state.db stays 403 when the active workspace is the Hermes home. Widen CSP-slice test window.
…make bare file:// rewrite code-fence-aware (Codex review #5) 1. api/routes.py: case-fold /api/media deny filename + dir containment checks (os.path.normcase + casefold) so STATE.DB / Sessions/ cannot bypass the state/secret deny on case-insensitive filesystems (macOS/Windows). 2. static/ui.js: move the bare file:// media-stash pass to run AFTER fenced-block and inline-code stashing, so a file:// inside a code block / backtick span stays literal text instead of becoming an auto-loaded <img>. The MEDIA: stash keeps its first-position precedent. Adds behavioral renderer tests (real renderMd via node) for fenced + inline code file:// staying literal, bare file:// becoming media, and anchors keeping the link path. Closes the last Codex review items for nesquena#3234.
…hecks, fix workspace over-block, protect raw <pre> (Codex review #6) 1. routes.py: hoist a single case-folded path helper (_norm/_within_ci/_equal_ci) used for ALL deny + carve-out comparisons (consistent macOS/Windows safety). 2. routes.py: split the deny into (a) dir-based denies that ALWAYS fire (even inside the active workspace — so a workspace overlapping a state dir cannot expose sessions/memories), and (b) filename denies relaxed only by the carve-out. Fix the over-block: a workspace that is a proper DESCENDANT of a Hermes root (e.g. STATE_DIR/workspace) is a legit project workspace and keeps the carve-out; only a root-itself / ancestor / $HOME / profiles / state-subdir workspace disables it. 3. ui.js: move the bare file:// media-stash pass after the raw-<pre> stash too, so file:// inside a raw <pre> block stays literal (not just fenced/inline code).
…TATE_DIR/workspace media (Codex review #7) The default workspace lives at STATE_DIR/workspace, so denying STATE_DIR itself 403'd legitimate workspace media. STATE_DIR is already in _hermes_roots, so its sensitive subdirs (STATE_DIR/sessions, /memories, /profiles, etc.) are still covered by the per-root subdir loop; direct sensitive files are still caught by the filename denies. Drop the wholesale _state_dir deny. Adds a regression test proving STATE_DIR/workspace/shot.png serves while STATE_DIR/sessions/*.json 403s.
…esale profiles deny (Codex review #8) Denying the whole <root>/profiles tree 403'd legitimate named-profile workspace media (<base>/profiles/p1/workspace/shot.png). Fix: remove 'profiles' from _DENY_SUBDIRS and instead enumerate each <root>/profiles/<name> directory as its own Hermes root — so each profile's sensitive subdirs (sessions/memories/cron/ logs/checkpoints/backups) + secret filenames are denied, while that profile's workspace/ is allowed via the carve-out. Adds a regression test: named-profile workspace media serves, profile + sibling-profile auth.json stay 403.
….json + .login_attempts.json (Codex review #9) Codex direct probe found three more auth-state basenames under STATE_DIR that /api/media still served: passkeys.json + .passkey_challenges.json (api/passkeys.py) and .login_attempts.json (api/auth.py). Add them to _DENY_FILENAMES.
…under Hermes roots (Codex review #10) auth.py/passkeys.py write via tmp*.<name>.tmp sidecars then rename; deny those suffixes (.sessions.tmp, .login_attempts.tmp, .passkeys.tmp, .passkey_challenges.tmp) under Hermes roots so a momentary temp file can't be fetched via /api/media.
…rs (Codex review #11) Per-profile WebUI state lives at <root>/webui_state (api/workspace.py), so <base>/profiles/<name>/webui_state/sessions/*.json was reachable — it is not a direct child of the profile root, so the prior deny-subdir loop missed it. Add <root>/webui_state/<state-subdir> to the deny dirs for every Hermes root. Adds a regression assertion (profile webui_state/sessions/*.json → 403).
Release stage-batchC → v0.51.183 (nesquena#3219 inline file:// media + nesquena#3234 /api/media secret-file confinement)
… transcribing - Settings toggle in Sound section (after voice mode button) - Mic button shows 'RAW' badge when raw audio mode is active - Raw mode: record → pending file → send() (auto-send if textarea empty) - Dictation mode: unchanged (transcribe → textarea) - 12 locales with full i18n keys and translations - 4 backend tests for upload + transcribe regression PR: Thinking Path - Hermes WebUI has dictation via Web Speech API / MediaRecorder → /api/transcribe - Telegram sends raw audio as attachment, agent decides what to do - Raw audio mode enables external STT, emotion/noise analysis, multimodal models What Changed - static/index.html: settingsRawAudio checkbox in Sound section - static/boot.js: raw audio preference, send raw blob as pending file - static/style.css: badge RAW on mic button - static/i18n.js: 12 locales with translations - tests/test_raw_audio_upload.py: 4 tests for upload + regressions AI Usage - Provider: opencode-go - Model: deepseek-v4-flash
…title The global _setButtonTooltip only sets data-tooltip, not data-i18n-title. The i18n system reads data-i18n-title for locale-aware tooltip updates. Switched to _setButtonTooltipAndKey which manages both attributes.
The .has-tooltip CSS uses ::after to show the tooltip text. Using ::after for the RAW badge overwrote the tooltip content. Fixed by adding a <span class="mic-raw-badge"> child element via JS. Also: use _setButtonTooltipAndKey for consistent i18n tooltip management.
…ests - 'Badge' naming was misleading since the visual badge was removed - Use monkeypatch.setitem with pytest fixture instead of manual import sys + del sys.modules (safer cleanup, no inline import) - All 9 tests pass
Bug: when raw audio mode is enabled, SpeechRecognition is skipped and MediaRecorder is used instead. But _stopMic() checked for a truthy 'recognition' reference (which exists even when unused) and called recognition.stop() — a no-op since speech recognition was never started — never reaching the mediaRecorder.stop() branch. Recording could never be stopped by the mic button. Fix: add !_rawAudioMode guard to the recognition.stop() branch.
…preserve (nesquena#3237) + non-POSIX test skip (nesquena#3235)
…w-audio mode (Codex review) Codex pre-release gate: clicking Send while raw-audio recording with text in the composer attached the audio but never sent — btnSend sets _micPendingSend=true before _stopMic, but _sendRawAudio only called send() when the textarea was empty. Mirror the transcribe path: if _micPendingSend is set, clear it and send() regardless of composer text; otherwise keep the empty-composer auto-send + toast. Co-authored-by: lucasrc <lrclucas@gmail.com>
…ode) — toggle-mid-record safety (Codex review #2) Codex round-2: _stopMic and mediaRecorder.onstop read the CURRENT _rawAudioMode to choose backend/dispatch, but the recording was started on the OLD mode — so toggling Settings→Sound mid-recording could stop the wrong backend (orphaning the other) or dispatch raw-vs-transcribe wrongly. Pin _activeCaptureMode (speech | media-raw | media-transcribe) at start; _stopMic + onstop use it. Adds front-end source-invariant regression tests. Co-authored-by: lucasrc <lrclucas@gmail.com>
Release stage-batchD → v0.51.184 (raw audio mode nesquena#3169 + scroll-preserve nesquena#3237 + non-POSIX test skip nesquena#3235)
Release DD: stage-batch14 — 4 PRs (replayed-context, interrupted-response, shutdown affordance, passkey opt-in) # Conflicts: # api/auth.py # api/routes.py # static/login.js
v0.51.184 — Release FD (stage-batchD): raw audio upload mode (nesquena#3169) + scroll-preserve on CLI import (nesquena#3237) + non-POSIX test skip (nesquena#3235) # Conflicts: # .github/workflows/docker-smoke.yml # api/auth.py # api/routes.py # api/updates.py # static/ui.js # tests/conftest.py # tests/test_update_apply_ui.py # tests/test_update_banner_fixes.py
The docker-smoke workflow built and cached ghcr.io/nesquena/hermes-webui but every compose file references ghcr.io/thecouchcoder-com/hermes-webui, so the build-image job's GHA layer cache fed no smoke variant and each variant rebuilt the image from scratch via a separate unconditional "Build local Dockerfile" step. Rename the cache image to the fork name in both the build-image job and the smoke "Restore from cache" step, and drop the redundant local build — the restore step now produces ghcr.io/thecouchcoder-com/hermes-webui:latest straight from the cache, so the multi-container variants exercise the PR's Dockerfile without a from-scratch rebuild. Single-container is unaffected (it uses build: .). Realizes the ~1-3 min/variant saving the comments already promised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Full catch-up sync of upstream
nesquena/hermes-webuiinto the fork: from our prior tip v0.51.131 all the way to the current upstream tip v0.51.184. After this lands, the fork is fully in sync with upstream (0 un-mergedv*tags).Opened/maintained manually because the scheduled
sync-upstreamworkflow cannot push branches that touch.github/workflows/— itsGITHUB_TOKENlacks theworkflowsscope (failed run #26694022064). Almost every upstream tag in this range touches a workflow file, which is why the backlog had grown to 50+ tags.Merge structure
Two
--no-ffmerge commits preserved for bisectability:Merge tag 'v0.51.132'— the next stage-by-stage tag (3 conflicts).Merge tag 'v0.51.184'— catch-up to tip (8 conflicts).Conflicts resolved
v0.51.132 (Tier 2):
api/auth.py(union PUBLIC_PATHS; folded multi-userhas_users()into upstream's newis_password_auth_enabled()),api/routes.py(/api/auth/statuskept ourfirst_boot/users_configuredgate + added passkey fields),static/login.js(kept multi-modeonSubmit, added passkey machinery, fixedinvalidPw→invalidCreds).v0.51.184 (Tier 2 / 2b / 3):
api/auth.py— kept fork dict-shape session persistence +_session_expiry/_session_user_idhelpers + the full RBAC surface (current_user/require_user/_ANONYMOUS_ADMIN/…), adopted upstream's new_SESSIONS_LOCKthread-safety.api/routes.py— kept our multi-user_handle_login_postdispatch; dropped upstream's inline single-password block.api/updates.py+static/ui.js+tests/test_update_*— selective merge: kept our fork-only origin-published-tags filter and per-target update design, folded in upstream's additive Update check falsely reports "1 release available" when local HEAD is ahead of latest fetched tag but behind a newer remote tag nesquena/hermes-webui#3140 fall-through guard and zero-target guard. Adapted two upstream Update check falsely reports "1 release available" when local HEAD is ahead of latest fetched tag but behind a newer remote tag nesquena/hermes-webui#3140 tests' git mocks to include our fork'sls-remote --tagscall (no upstream test dropped).tests/conftest.py— union (our PBKDF2 fast-hash/unsigned-cookie env + upstream's profile-isolation fixtures)..github/workflows/docker-smoke.yml(Tier 3) — kept our fork-only "Build local Dockerfile" retag step.Note on the split webui/agent update feature
The fork goal of split WebUI/Agent updates has converged with upstream (PR nesquena#2843 "Ignore Agent updates", PR nesquena#2893 zero-target guard). This PR keeps our per-target implementation rather than swapping to upstream's single-banner internals, because the user-facing split is preserved either way and our structure is what the rest of the fork's code/tests depend on. The fork-only origin-published-tags fix (
_origin_release_tags) is retained — it's structural to being a fork that sync-merges upstream tags origin never publishes.Validation
pytest tests/ -q --timeout=60— 7114 passed, 100 skipped, 3 xpassed, 0 failed (~470 new tests from the v0.51.184 range, all green).Follow-ups (not in this PR)
workflowscope. Until then, syncs touching.github/workflows/must be pushed manually.docker-smoke.ymlstill has someghcr.io/nesquena/...image references alongside ourthecouchcoder-comretag; pre-existing, worth reconciling separately.🤖 Generated with Claude Code