diff --git a/CHANGELOG.md b/CHANGELOG.md index 6115c51f..42c77166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed +- **PR #2140** by @franksong2702 — WebUI fallback provider activation now preserves `api_key` and `key_env` from `fallback_model` / `fallback_providers` entries before handing them to Hermes Agent, so env-backed fallback credentials can be resolved after a primary provider 401 instead of failing as an unauthenticated fallback. Closes #2133. + - **PR #2117** by @ayushere — `ctl.sh start` no longer crashes on macOS (bash 3.2) with `preserved[@]: unbound variable`. The dotenv-preserve loop in `_load_repo_dotenv_preserving_env()` iterated `"${preserved[@]}"` under `set -euo pipefail`, which bash 4+ silently allows on empty arrays but bash 3.2 (still the default `/usr/bin/bash` on macOS) treats as an unbound-variable error. Guards the iteration with `if [[ ${#preserved[@]} -gt 0 ]]; then ... fi` — matches the canonical bash 3.2 strict-mode pattern. This is the third bash 3.2 compat fix to land in `ctl.sh` (prior: `025f137f` guarded `CTL_BOOTSTRAP_ARGS[@]` with the `${arr[@]+...}` pass-through pattern, `630981a0` replaced `[[ -v ${key} ]]` with `[[ -n "${!key+x}" ]]`). Defense-in-depth: added `tests/test_ctl_bash32_compat.py` (5 static-pattern regressions) pinning both empty-array guards plus a denylist for bash 4+ syntax (`declare -A`, `mapfile`, `[[ -v ]]`, `${var^^}`, `${var,,}`) so the next regression surfaces in CI instead of a macOS user's terminal. Stage-343 reviewer added the regression-test file alongside the contributor's 5-LOC fix to ctl.sh. ## [v0.51.49] — 2026-05-12 — Release Y (stage-342 — 3-PR contributor batch — read-only worktree status endpoint + worktree-retained response preference + Codex quota credential-pool fallback) diff --git a/api/streaming.py b/api/streaming.py index fd772414..7a90d51e 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -2770,6 +2770,8 @@ def _run_agent_streaming( 'model': _fb_entry.get('model', ''), 'provider': _fb_entry.get('provider', ''), 'base_url': _fb_entry.get('base_url'), + 'api_key': _fb_entry.get('api_key'), + 'key_env': _fb_entry.get('key_env'), } # Build kwargs defensively — guard newer params so the WebUI diff --git a/tests/test_pr1339_fallback_providers_list.py b/tests/test_pr1339_fallback_providers_list.py index c180c8dd..c0a804f5 100644 --- a/tests/test_pr1339_fallback_providers_list.py +++ b/tests/test_pr1339_fallback_providers_list.py @@ -70,3 +70,21 @@ def test_fallback_resolved_initialized_to_none(): assert "_fallback_resolved = None" in block, ( "_fallback_resolved must be initialized to None so callers can rely on its presence" ) + + +def test_fallback_resolved_preserves_credential_hints(): + """Fallback entries must keep credential hints for AIAgent fallback activation.""" + block = _extract_fallback_block() + resolved_start = block.find("_fallback_resolved = {") + assert resolved_start != -1, "_fallback_resolved dict not found" + resolved_end = block.find("}", resolved_start) + resolved_dict = block[resolved_start:resolved_end] + + assert "'api_key': _fb_entry.get('api_key')" in resolved_dict, ( + "WebUI must preserve fallback_model/fallback_providers api_key so " + "AIAgent._try_activate_fallback can authenticate the fallback." + ) + assert "'key_env': _fb_entry.get('key_env')" in resolved_dict, ( + "WebUI must preserve fallback_model/fallback_providers key_env so " + "AIAgent._try_activate_fallback can resolve env-backed fallback keys." + )