Install-time-only bootstrap fix: discover agent installed as root (FHS /usr/local/lib/hermes-agent) or behind the bash launcher. Independently reviewed + approved (nesquena, OWNER); CI green on all 9 shards + browser-smoke + lint. No version bump (no runtime/agent change).
A fresh root install on Linux (e.g. a new Proxmox/Ubuntu VM) places the Hermes
Agent at /usr/local/lib/hermes-agent and links the CLI into /usr/local/bin,
while the modern `hermes` launcher is a `#!/usr/bin/env bash` wrapper that
exec's the venv entrypoint. The old discovery only checked
~/.hermes/hermes-agent-style paths and parsed the launcher's shebang as a Python
interpreter, so on a root VM discover_agent_dir() returned None, bootstrap built
a WebUI-only .venv, and chat aborted at launch with "Python environment cannot
import both WebUI dependencies and Hermes Agent."
Two complementary fixes:
- Add /usr/local/lib/hermes-agent to discover_agent_dir()'s candidate list.
- Harden _agent_dir_from_hermes_cli() to follow the launcher's exec target (any
absolute path quoted in the wrapper) up to run_agent.py, not just a Python
shebang. Skips /usr/bin/env indirection.
Now `python3 bootstrap.py` works out of the box for root/container installs with
no HERMES_WEBUI_PYTHON override needed. Adds tests for the bash-wrapper shape,
the env-indirection no-false-positive case, and the FHS candidate probe; updates
docs/troubleshooting.md.
allenliang2022: recovered assistant rows were added to the visible transcript but not context_messages,
so the model lost recovered output. Adds _append_recovered_turn_to_context with a role-dedup guard.
Co-authored-by: allenliang2022 <allenliang2022@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>
Manual /compress wrote the compressed payload into BOTH s.messages (visible) and s.context_messages
(model-facing) + wiped s.tool_calls -> messages vanished from the window. Fix keeps s.messages +
tool history durable, deep-copies compressed only into s.context_messages, anchors off visible msgs.
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.
Codex re-gate (CORE, reproduced): _reload_dotenv() was guarded, but get_profile_runtime_env() also
copies a profile .env into runtime env, which profile_env_for_background_worker()/streaming apply to
os.environ -> HERMES_WEBUI_ISOLATED_PROFILE=0 in a profile .env still escaped isolation on that path.
Fix: filter _PROTECTED_ENV_KEYS in get_profile_runtime_env's .env loop + add the key to
_BLOCKED_RUNTIME_ENV_KEYS (gateway-parity filter). + runtime-path regression test.
Codex gate (CORE, reproduced): _reload_dotenv() copies a profile's .env into os.environ, so a
contained user could set HERMES_WEBUI_ISOLATED_PROFILE=0 in their own profile .env and silently
escape isolation. Fix: _PROTECTED_ENV_KEYS — _reload_dotenv refuses to override operator/deployment
keys from a profile .env (logs a warning). + regression test proving .env can't disable isolation.
Profile-isolation regression: isolated mode was inferred from HERMES_HOME path shape (*/profiles/<name>),
which the launcher exports for ANY normal named profile -> single-users on a named profile lost profile
switching + saw only 1 profile. Fix: HERMES_WEBUI_ISOLATED_PROFILE explicit opt-in is now the PRIMARY gate.