diff --git a/CHANGELOG.md b/CHANGELOG.md index b6df1a29..7f2fa655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Fixed +- `start.sh` now loads non-readonly `.env` keys after filtering Docker-style `UID`/`GID` entries on macOS/bash combinations where `source <(grep ...)` produced an empty sourced stream. + - **PR #2234** by @Jordan-SkyLF (refines #2207) — Three update-banner improvements: (1) Update summaries no longer repeat the same bullet under both "What you'll notice" and "Worth knowing" — visible notice items keep priority, and the secondary section is omitted when there is no distinct detail to show. (2) Update summaries now cap large commit lists (24 + probe item) before sending them to the summarizer and disclose when the summary uses only the latest commit subjects, while keeping the full comparison link available — bounds summarizer cost on large update ranges while remaining honest about coverage. (3) Update banners now wrap generated-summary links and long update text on narrow/mobile screens inside the banner instead of pushing controls off-screen. 108-line regression coverage for short-target dedup, repeated Agent-summary bullets, large-range capped input, and responsive wrapping. - **PR #2236** by @jasonjcwu — Silent failure detection in `api/streaming.py` now scans only NEW messages, not the full conversation history. Pre-fix, the `_assistant_added` check at `_run_agent_streaming` scanned all messages in `result["messages"]` (including pre-turn history); if any prior turn contained an assistant response, `_assistant_added` was `True` and the apperror SSE event was silently skipped, leaving the user staring at a blank response after a provider 401/429/rate-limit error. Fix extracts a `_has_new_assistant_reply(all_messages, prev_count)` helper that only inspects messages beyond the pre-turn history offset (`_previous_context_messages`); applied to both the main detection path and the self-heal/retry `_heal_ok` check. 15-test regression suite covering empty/short/long-history scenarios, the heal path, and the `len < prev_count` edge-case fallback. Also includes a small alignment fix to `test_issue1857_usage_overwrite.py` so the FakeAgent message shape matches what the real agent produces. diff --git a/start.sh b/start.sh index a1406663..8a37bf40 100755 --- a/start.sh +++ b/start.sh @@ -40,10 +40,14 @@ if [[ -f "${REPO_ROOT}/.env" ]]; then # both `source` and `.env`. # Sourced from PR #1686 (@binhpt310) — Cluster 1 (operational hardening), # extracted to a focused follow-up after the parent PR was deferred. + _hermes_env_filtered="$(mktemp "${TMPDIR:-/tmp}/hermes-webui-env.XXXXXX")" + grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env" > "${_hermes_env_filtered}" || true set -a # shellcheck source=/dev/null - source <(grep -vE '^[[:space:]]*(export[[:space:]]+)?(UID|GID|EUID|EGID|PPID)=' "${REPO_ROOT}/.env") + source "${_hermes_env_filtered}" set +a + rm -f "${_hermes_env_filtered}" + unset _hermes_env_filtered fi PYTHON="${HERMES_WEBUI_PYTHON:-}" diff --git a/tests/test_docker_env_readonly_vars.py b/tests/test_docker_env_readonly_vars.py index 226f0b8d..42d07dd1 100644 --- a/tests/test_docker_env_readonly_vars.py +++ b/tests/test_docker_env_readonly_vars.py @@ -66,23 +66,16 @@ class TestStartShReadonlyEnvFilter: f"'{var}: readonly variable'" ) - def test_filter_pattern_uses_grep_or_equivalent(self): + def test_filter_pattern_uses_grep_before_source(self): """Filter must use a pattern that strips readonly-var lines before - the bash `source` consumes them. `grep -vE` is the canonical form; - the assertion accepts any process-substitution-into-source shape.""" - # Look for `source <(...UID...)` pattern. Note that the inner shell - # expression can contain its own parens (e.g. `(export[[:space:]]+)`), - # so we use a non-greedy `.*?` rather than `[^)]*`. - assert re.search( - r"source\s+<\(.*?UID.*?\)", - START_SH, - re.DOTALL, - ), ( - "start.sh's .env loader must filter readonly bash vars " - "(UID/GID/EUID/EGID/PPID) via `source <(grep -vE ...)` or " - "equivalent process-substitution form before `source`-ing " - "the .env file" - ) + the bash `source` consumes them. The loader may use a temporary file + rather than process substitution because some bash/macOS combinations + can source an empty `/dev/fd/*` stream from `source <(grep ...)`.""" + grep_idx = START_SH.find("grep -vE") + source_idx = START_SH.find("source", grep_idx) + assert grep_idx != -1, "start.sh must filter readonly vars with grep -vE or equivalent" + assert source_idx != -1, "start.sh must source the filtered .env stream" + assert grep_idx < source_idx, "readonly-var filtering must happen before source" def test_filter_handles_optional_export_prefix(self): """The ``export`` prefix on env vars is optional but common. The