From 92d845ce9d0b56a120c9740e4015e2b994ec9821 Mon Sep 17 00:00:00 2001 From: EloquentBrush0x <283442588+EloquentBrush0x@users.noreply.github.com> Date: Sat, 16 May 2026 02:59:39 +0300 Subject: [PATCH 01/10] fix(gateway): add trust_env=True to aiohttp sessions in SMS, Slack, Teams, Google Chat adapters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aiohttp.ClientSession defaults to trust_env=False, which silently ignores HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY environment variables. Users behind a corporate or network proxy cannot reach external APIs on any of these platforms — all outbound requests fail with connection errors. Symmetric with wecom.py (line 276), weixin.py (lines 1055/1268/1274), and matrix.py (no-proxy path) which already set this flag. Complements the open LINE fix (#26635) with the remaining gateway and plugin adapters. Changed: - gateway/platforms/sms.py: persistent Twilio session (connect) + fallback session (send) — both hit https://api.twilio.com - gateway/platforms/slack.py: ephemeral response_url POST session — hits https://hooks.slack.com/... callback URLs - plugins/platforms/teams/adapter.py: standalone send session — hits login.microsoftonline.com (token) + Bot Framework service URL - plugins/platforms/google_chat/adapter.py: standalone send session — hits https://chat.googleapis.com/v1/... WhatsApp sessions are excluded: they connect to http://127.0.0.1:{port} (local bridge) and must not be routed through a system proxy. --- gateway/platforms/slack.py | 2 +- gateway/platforms/sms.py | 2 ++ plugins/platforms/google_chat/adapter.py | 2 +- plugins/platforms/teams/adapter.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 2116b569f9683..5accfdb410899 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -482,7 +482,7 @@ async def _send_slash_ephemeral( "text": text, } try: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(trust_env=True) as session: async with session.post( ctx["response_url"], json=payload, diff --git a/gateway/platforms/sms.py b/gateway/platforms/sms.py index 2cf7db69b74e2..9d9957d5ea16c 100644 --- a/gateway/platforms/sms.py +++ b/gateway/platforms/sms.py @@ -128,6 +128,7 @@ async def connect(self) -> bool: await site.start() self._http_session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=30), + trust_env=True, ) self._running = True @@ -169,6 +170,7 @@ async def send( session = self._http_session or aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=30), + trust_env=True, ) try: for chunk in chunks: diff --git a/plugins/platforms/google_chat/adapter.py b/plugins/platforms/google_chat/adapter.py index d8777bf710126..1520d6664eb5f 100644 --- a/plugins/platforms/google_chat/adapter.py +++ b/plugins/platforms/google_chat/adapter.py @@ -3246,7 +3246,7 @@ async def _standalone_send( return {"error": "Google Chat standalone send: aiohttp not installed"} try: - async with _aiohttp.ClientSession(timeout=_aiohttp.ClientTimeout(total=30.0)) as session: + async with _aiohttp.ClientSession(timeout=_aiohttp.ClientTimeout(total=30.0), trust_env=True) as session: async with session.post( url, json=body, diff --git a/plugins/platforms/teams/adapter.py b/plugins/platforms/teams/adapter.py index c71baeb9d93ee..f8a1dc3d5b4a8 100644 --- a/plugins/platforms/teams/adapter.py +++ b/plugins/platforms/teams/adapter.py @@ -566,7 +566,7 @@ async def _standalone_send( # Per-request timeouts so a slow STS endpoint cannot starve the # subsequent activity POST of its budget. per_request_timeout = _aiohttp.ClientTimeout(total=15.0) - async with _aiohttp.ClientSession() as session: + async with _aiohttp.ClientSession(trust_env=True) as session: async with session.post( token_url, data={ From 54de7052b8a6231c7c4a5db40fd8f282a630225c Mon Sep 17 00:00:00 2001 From: subtract0 <205509009+subtract0@users.noreply.github.com> Date: Sat, 16 May 2026 23:09:31 -0700 Subject: [PATCH 02/10] fix(gateway): avoid zsh status variable in update wrapper --- gateway/run.py | 6 +++++- tests/gateway/test_update_streaming.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gateway/run.py b/gateway/run.py index 56185190e26bf..81ce914b8abf7 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -12837,7 +12837,11 @@ async def _handle_update_command(self, event: MessageEvent) -> str: update_cmd = ( f"PYTHONUNBUFFERED=1 {hermes_cmd_str} update --gateway" f" > {shlex.quote(str(output_path))} 2>&1; " - f"status=$?; printf '%s' \"$status\" > {shlex.quote(str(exit_code_path))}" + # Avoid `status=$?`: `status` is a read-only special parameter + # in zsh, and this command string is copied/reused in macOS/zsh + # operator wrappers. Keep the template zsh-safe even though this + # specific subprocess currently runs under bash. + f"rc=$?; printf '%s' \"$rc\" > {shlex.quote(str(exit_code_path))}" ) setsid_bin = shutil.which("setsid") if setsid_bin: diff --git a/tests/gateway/test_update_streaming.py b/tests/gateway/test_update_streaming.py index 932bd1b057903..eb0f0cfa89051 100644 --- a/tests/gateway/test_update_streaming.py +++ b/tests/gateway/test_update_streaming.py @@ -237,6 +237,8 @@ async def test_spawns_with_gateway_flag(self, tmp_path): cmd_string = call_args[-1] if isinstance(call_args, list) else str(call_args) assert "--gateway" in cmd_string assert "PYTHONUNBUFFERED" in cmd_string + assert "rc=$?" in cmd_string + assert "status=$?" not in cmd_string assert "stream progress" in result From 03ed88c16827c06dc85a22df568144be600fd6f1 Mon Sep 17 00:00:00 2001 From: zwolniony <12735938+zwolniony@users.noreply.github.com> Date: Sat, 16 May 2026 23:09:31 -0700 Subject: [PATCH 03/10] Local: doctor uses x-goog-api-key for Google generativelanguage endpoint --- hermes_cli/doctor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 9d3b6e3c01ad7..07aaa2e38bc5d 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -1474,6 +1474,15 @@ def _probe_apikey_provider(pname, env_vars, default_url, base_env, } if base_url_host_matches(base, "api.kimi.com"): headers["User-Agent"] = "claude-code/0.1.0" + # Google's Generative Language API (generativelanguage.googleapis.com) + # rejects ``Authorization: Bearer `` with 401 + # ``ACCESS_TOKEN_TYPE_UNSUPPORTED`` — that header is reserved for + # OAuth 2 access tokens, not plain API keys. Plain keys use + # ``x-goog-api-key`` (or ``?key=``). Without this, a perfectly valid + # GOOGLE_API_KEY/GEMINI_API_KEY always shows red in ``hermes doctor``. + if url and base_url_host_matches(url, "generativelanguage.googleapis.com"): + headers.pop("Authorization", None) + headers["x-goog-api-key"] = key r = httpx.get(url, headers=headers, timeout=10) if ( pname == "Alibaba/DashScope" From a9b49e3d97718e919c1115a6d9d3b17784e23022 Mon Sep 17 00:00:00 2001 From: Ambuj Kumar Date: Sat, 16 May 2026 02:23:25 +0530 Subject: [PATCH 04/10] fix(gateway): preserve underscores in plain-text identifiers --- gateway/platforms/helpers.py | 4 ++-- tests/gateway/test_bluebubbles.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/helpers.py b/gateway/platforms/helpers.py index 1c4f451585ae5..a3704bf50cf0f 100644 --- a/gateway/platforms/helpers.py +++ b/gateway/platforms/helpers.py @@ -168,8 +168,8 @@ def cancel_all(self) -> None: # Pre-compiled regexes for performance _RE_BOLD = re.compile(r"\*\*(.+?)\*\*", re.DOTALL) _RE_ITALIC_STAR = re.compile(r"\*(.+?)\*", re.DOTALL) -_RE_BOLD_UNDER = re.compile(r"__(.+?)__", re.DOTALL) -_RE_ITALIC_UNDER = re.compile(r"_(.+?)_", re.DOTALL) +_RE_BOLD_UNDER = re.compile(r"\b__(?![\s_])(.+?)(? Date: Thu, 14 May 2026 22:37:51 -0700 Subject: [PATCH 05/10] feat(gateway): extract auto-TTS markdown strip into prepare_tts_text() hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the inlined `re.sub(...)[:4000].strip()` cleanup at the auto-TTS site in `_process_message_background` into an overridable method `BasePlatformAdapter.prepare_tts_text(text: str) -> str`. The default implementation is byte-identical to the previous inline expression — strip `* _ \` # [ ] ( )` and truncate to 4000 chars — so every existing adapter (Telegram, Discord, Slack, Matrix, IRC, etc.) gets exactly the same behaviour as before. Zero behaviour change for any consumer that doesn't override the method. Why add the hook: voice-first platform adapters need stricter cleanup than text-bubble platforms. The default strips a handful of markdown sigils, which is fine when the output goes into a Discord embed or a Telegram message bubble — but read aloud by a TTS engine, URLs (`https://example.com/foo`), fenced code blocks, file paths (`/Users/x/foo.py`), and `MEDIA:` tags turn into long sequences of unintelligible characters. With this hook an adapter can drop those spans before TTS while leaving the data-channel transcript intact for visual rendering. Without the hook, voice adapters have to either - duplicate the auto-TTS flow inside their own `handle_response` pipeline, which means re-implementing the entire `extract_media`, `extract_images`, `extract_local_files`, attachment routing and error-handling sequence in `_process_message_background`, or - live with TTS speaking URLs character-by-character. Both are worse than a 7-line method addition. Example consumer: https://github.com/kortexa-ai/hermes-livekit — LiveKit WebRTC voice gateway plugin. Its `LiveKitAdapter.prepare_tts_text()` additionally strips fenced code blocks, inline code, URLs, file paths, and `MEDIA:` tags before TTS synthesis, while the full response still reaches connected clients via the data channel. Drop-in installable via `pip install git+https://github.com/kortexa-ai/hermes-livekit.git`. Carved out of #3894 (LiveKit WebRTC gateway PR) so the generic hook can land independently of the LiveKit platform itself. --- gateway/platforms/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 7b3147e21f45c..96b56d29cc7d3 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -2014,6 +2014,13 @@ async def send_voice( text = f"{caption}\n{text}" return await self.send(chat_id=chat_id, content=text, reply_to=reply_to, metadata=metadata) + def prepare_tts_text(self, text: str) -> str: + """Prepare text for TTS. Override to filter tool output, code, etc. + + Default strips markdown formatting and truncates to 4000 chars. + """ + return re.sub(r'[*_`#\[\]()]', '', text)[:4000].strip() + async def play_tts( self, chat_id: str, @@ -3144,7 +3151,7 @@ async def _stop_typing_task() -> None: from tools.tts_tool import text_to_speech_tool, check_tts_requirements if check_tts_requirements(): import json as _json - speech_text = re.sub(r'[*_`#\[\]()]', '', text_content)[:4000].strip() + speech_text = self.prepare_tts_text(text_content) if not speech_text: raise ValueError("Empty text after markdown cleanup") tts_result_str = await asyncio.to_thread( From be747db5543f81f5bb45d39684bf20da694f2d6e Mon Sep 17 00:00:00 2001 From: zccyman Date: Thu, 14 May 2026 07:49:52 +0800 Subject: [PATCH 06/10] fix(auxiliary): resolve api_key_env alias in named custom provider path of resolve_provider_client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In resolve_provider_client(), the named custom provider code path at ~line 2914 only checked the ``key_env`` field when looking for an environment-variable-based API key. The documented ``api_key_env`` snake_case alias was silently ignored, causing custom providers configured with ``api_key_env`` to fall through to the ``no-key-required`` placeholder — which produces a confusing 401 (``****ired`` mask) on auth-required remote endpoints. This mirrors the same fix already applied to run_agent.py in commit 6ddc48b05 (fix(fallback): resolve api_key_env in fallback chain entries). Also adds a logger.warning() when the placeholder is reached, so future alias gaps are easier to debug. Closes #25091 --- agent/auxiliary_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index cfc44e5f2a656..102ff79f1cecc 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -3049,10 +3049,17 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "", if custom_entry: custom_base = custom_entry.get("base_url", "").strip() custom_key = custom_entry.get("api_key", "").strip() - custom_key_env = custom_entry.get("key_env", "").strip() + custom_key_env = (custom_entry.get("key_env") or custom_entry.get("api_key_env") or "").strip() if not custom_key and custom_key_env: custom_key = os.getenv(custom_key_env, "").strip() custom_key = custom_key or "no-key-required" + if custom_key == "no-key-required": + logger.warning( + "resolve_provider_client: named custom provider %r has no resolvable " + "api_key — request will be sent with placeholder no-key-required " + "and will 401 on auth-required endpoints", + custom_entry.get("name") or provider, + ) # An explicit per-task api_mode override (from _resolve_task_provider_model) # wins; otherwise fall back to what the provider entry declared. entry_api_mode = (api_mode or custom_entry.get("api_mode") or "").strip() From aca5ad09d9cf7adff76d8506e83d6ce0f8ab4726 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 16 May 2026 16:41:03 +0900 Subject: [PATCH 07/10] [agent] fix: harden api server response headers --- gateway/platforms/api_server.py | 5 +++++ tests/gateway/test_api_server.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index ebd4f014690b6..0668896e170f7 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -510,7 +510,12 @@ async def body_limit_middleware(request, handler): body_limit_middleware = None # type: ignore[assignment] _SECURITY_HEADERS = { + "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "X-XSS-Protection": "0", "Referrer-Policy": "no-referrer", } diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index 7d08d64bb321c..aae5f55053207 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -445,7 +445,12 @@ async def test_security_headers_present(self, adapter): async with TestClient(TestServer(app)) as cli: resp = await cli.get("/health") assert resp.status == 200 + assert resp.headers.get("Content-Security-Policy") == "default-src 'none'; frame-ancestors 'none'" + assert resp.headers.get("Permissions-Policy") == "camera=(), microphone=(), geolocation=()" + assert resp.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains" assert resp.headers.get("X-Content-Type-Options") == "nosniff" + assert resp.headers.get("X-Frame-Options") == "DENY" + assert resp.headers.get("X-XSS-Protection") == "0" assert resp.headers.get("Referrer-Policy") == "no-referrer" @pytest.mark.asyncio From 020ebc45e8868e0ed6ce9030b7722885b71368ed Mon Sep 17 00:00:00 2001 From: phoenixshen <1594534+phoenixshen@users.noreply.github.com> Date: Sat, 16 May 2026 23:09:31 -0700 Subject: [PATCH 08/10] fix: respect user-configured vision model for OpenRouter _OPENROUTER_MODEL hardcoded 'google/gemini-3-flash-preview' which returns 404 on OpenRouter, breaking all vision tasks for users who rely on the OpenRouter default. Additionally, _try_openrouter() ignored the user-configured auxiliary.vision.model entirely. Changes: - Update _OPENROUTER_MODEL default to google/gemini-2.5-flash (valid) - Add optional 'model' parameter to _try_openrouter() - Pass configured model from _resolve_strict_vision_backend() through to _try_openrouter() This allows users who set auxiliary.vision.model (e.g. x-ai/grok-4.3) to have it actually used, while maintaining backward compatibility. --- agent/auxiliary_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 102ff79f1cecc..e02fa1911f7fe 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -424,7 +424,7 @@ def _nous_extra_body() -> dict: auxiliary_is_nous: bool = False # Default auxiliary models per provider -_OPENROUTER_MODEL = "google/gemini-3-flash-preview" +_OPENROUTER_MODEL = "google/gemini-2.5-flash" _NOUS_MODEL = "google/gemini-3-flash-preview" _NOUS_DEFAULT_BASE_URL = "https://inference-api.nousresearch.com/v1" _ANTHROPIC_DEFAULT_BASE_URL = "https://api.anthropic.com" @@ -1473,7 +1473,7 @@ def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: -def _try_openrouter(explicit_api_key: str = None) -> Tuple[Optional[OpenAI], Optional[str]]: +def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Optional[OpenAI], Optional[str]]: pool_present, entry = _select_pool_entry("openrouter") if pool_present: or_key = explicit_api_key or _pool_runtime_api_key(entry) @@ -1483,7 +1483,7 @@ def _try_openrouter(explicit_api_key: str = None) -> Tuple[Optional[OpenAI], Opt base_url = _pool_runtime_base_url(entry, OPENROUTER_BASE_URL) or OPENROUTER_BASE_URL logger.debug("Auxiliary client: OpenRouter via pool") return OpenAI(api_key=or_key, base_url=base_url, - default_headers=build_or_headers()), _OPENROUTER_MODEL + default_headers=build_or_headers()), model or _OPENROUTER_MODEL or_key = explicit_api_key or os.getenv("OPENROUTER_API_KEY") if not or_key: @@ -1491,7 +1491,7 @@ def _try_openrouter(explicit_api_key: str = None) -> Tuple[Optional[OpenAI], Opt return None, None logger.debug("Auxiliary client: OpenRouter") return OpenAI(api_key=or_key, base_url=OPENROUTER_BASE_URL, - default_headers=build_or_headers()), _OPENROUTER_MODEL + default_headers=build_or_headers()), model or _OPENROUTER_MODEL def _describe_openrouter_unavailable() -> str: @@ -3407,7 +3407,7 @@ def _resolve_strict_vision_backend( if provider == "copilot": return resolve_provider_client("copilot", model, is_vision=True) if provider == "openrouter": - return _try_openrouter() + return _try_openrouter(model=model) if provider == "nous": return _try_nous(vision=True) if provider == "openai-codex": From ef379fb5b1f8521d6cf2c1706ff29878c323b2e6 Mon Sep 17 00:00:00 2001 From: AhmetArif0 <147827411+AhmetArif0@users.noreply.github.com> Date: Sat, 16 May 2026 02:06:31 +0300 Subject: [PATCH 09/10] fix(line): add trust_env=True to all _LineClient aiohttp sessions _LineClient's five aiohttp.ClientSession() calls omit trust_env=True, silently bypassing HTTP_PROXY / HTTPS_PROXY / ALL_PROXY. Result: every LINE API call (reply, push, loading, fetch_content, get_bot_user_id) ignores the system proxy. Fix: add trust_env=True to all five session constructions. Symmetric with the wecom and weixin adapters which already set this flag. No behavior change for users not behind a proxy. --- plugins/platforms/line/adapter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/platforms/line/adapter.py b/plugins/platforms/line/adapter.py index db5d3564d3297..907f16be4ff38 100644 --- a/plugins/platforms/line/adapter.py +++ b/plugins/platforms/line/adapter.py @@ -447,7 +447,7 @@ def __init__(self, channel_access_token: str, *, timeout: float = 15.0) -> None: async def reply(self, reply_token: str, messages: List[Dict[str, Any]]) -> None: import aiohttp timeout = aiohttp.ClientTimeout(total=self._timeout) - async with aiohttp.ClientSession(timeout=timeout) as session: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with session.post( LINE_REPLY_URL, headers=self._headers, @@ -460,7 +460,7 @@ async def reply(self, reply_token: str, messages: List[Dict[str, Any]]) -> None: async def push(self, chat_id: str, messages: List[Dict[str, Any]]) -> None: import aiohttp timeout = aiohttp.ClientTimeout(total=self._timeout) - async with aiohttp.ClientSession(timeout=timeout) as session: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with session.post( LINE_PUSH_URL, headers=self._headers, @@ -479,7 +479,7 @@ async def loading(self, chat_id: str, seconds: int = 60) -> None: clamped = max(5, min(60, (seconds // 5) * 5 or 5)) try: timeout = aiohttp.ClientTimeout(total=5.0) - async with aiohttp.ClientSession(timeout=timeout) as session: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: await session.post( LINE_LOADING_URL, headers=self._headers, @@ -493,7 +493,7 @@ async def fetch_content(self, message_id: str) -> bytes: import aiohttp url = LINE_CONTENT_URL_FMT.format(message_id=message_id) timeout = aiohttp.ClientTimeout(total=30.0) - async with aiohttp.ClientSession(timeout=timeout) as session: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with session.get(url, headers={"Authorization": f"Bearer {self._token}"}) as resp: if resp.status >= 400: raise RuntimeError(f"LINE content {resp.status}") @@ -504,7 +504,7 @@ async def get_bot_user_id(self) -> Optional[str]: import aiohttp timeout = aiohttp.ClientTimeout(total=10.0) try: - async with aiohttp.ClientSession(timeout=timeout) as session: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with session.get(LINE_BOT_INFO_URL, headers=self._headers) as resp: if resp.status >= 400: return None From 6ab595debeb273bf9f319ecb297fcf1d92081b8e Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 16 May 2026 23:10:34 -0700 Subject: [PATCH 10/10] chore(release): AUTHOR_MAP entries for batch salvage group 4 contributors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds release-note attribution mappings for 9 contributors from group 4: - @EloquentBrush0x (PR #26657) - @subtract0 (PR #25658) - @zwolniony (PR #26961) - @that-ambuj (PR #26582) - @zccyman (PR #25294) - @lidge-jun (PR #26814) - @phoenixshen (PR #26768) - @AhmetArif0 (PR #26635) - (francip already mapped from prior PR #26134 attribution) #27147 dropped from this batch — already landed on main as 4b17c2411. --- scripts/release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 52da4c2f4b7c3..c388116cff6fa 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1126,6 +1126,17 @@ "pr7426@users.noreply.github.com": "pr7426", # PR #27048 (cron parallel job loss) "rahulnilvan43@gmail.com": "therahul-yo", # PR #26215 (mock keychain in tests) "kingsleyemeka117@gmail.com": "flamiinngo", # PR #27205 (UnicodeEncodeError footgun checker) + # batch salvage (May 2026 LHF run, group 4) + "283442588+EloquentBrush0x@users.noreply.github.com": "EloquentBrush0x", # PR #26657 (trust_env aiohttp) + "205509009+subtract0@users.noreply.github.com": "subtract0", # PR #25658 (zsh $status -> $rc) + "patryk@jarmakowicz.me": "zwolniony", # PR #26961 (gemini x-goog-api-key) + "12735938+zwolniony@users.noreply.github.com": "zwolniony", + "ambuj@dodopayments.com": "that-ambuj", # PR #26582 (preserve underscores) + "zccyman@163.com": "zccyman", # PR #25294 (custom provider api_key_env alias) + "bitkyc08@gmail.com": "lidge-jun", # PR #26814 (api server browser security headers) + "sp_ps@Mac-mini.lan": "phoenixshen", # PR #26768 (respect user-configured vision model) + "1594534+phoenixshen@users.noreply.github.com": "phoenixshen", + "147827411+AhmetArif0@users.noreply.github.com": "AhmetArif0", # PR #26635 (line proxy env vars) }